String.split()临时对象和垃圾收集

时间:2016-05-03 14:39:14

标签: java string performance split garbage-collection

在我的项目中,我们需要读取一个非常大的文件,其中每一行都有一个由特殊字符分隔的标识符(" |")。不幸的是,我不能使用并行性,因为有必要在一行的最后一个字符与下一行的第一个字符之间进行验证,以决定是否提取它。无论如何,要求非常简单:将行分解为标记,分析它们并仅将其中一些存储在内存中。代码非常简单,如下所示:

final LineIterator iterator = FileUtils.lineIterator(file)
while(iterator.hasNext()){
   final String[] tokens = iterator.nextLine().split("\\|");
   //process
}

但是这段代码非常非常低效。 split()方法生成了太多未收集的临时对象(如下所述:http://chrononsystems.com/blog/hidden-evils-of-javas-stringsplit-and-stringr

为了进行比较:一个5mb的文件在文件处理结束时使用了大约35mb的内存。

我测试了一些替代方案,如:

但它们似乎都不够有效。使用JProfiler,我可以看到临时对象使用的内存量太高(使用了35 mb,但实际上只有15 mb被有效对象使用)。

然后我决定做一个简单的测试:在读取50,000行之后,显式调用System.gc()。然后,在进程结束时,内存使用量从35mb减少到16mb。我测试了很多次,总是得到相同的结果。

我知道调用System.gc()是一种不好的做法(如Why is it bad practice to call System.gc()?所示)。但是在cenario中还有其他任何替代方法,可以调用split()方法数百万次吗?

[UPDATE] 我仅将5 MB文件用于测试目的,但系统应处理更大的文件(500Mb~1Gb)

2 个答案:

答案 0 :(得分:1)

这里要说的第一个也是最重要的一点是,不要担心。 JVM消耗35MB的RAM,因为它的配置表明它的数量足够低。当它的高效GC算法决定它的时间时,它会扫除所有这些物体,没问题。

如果您真的想要,可以使用内存管理选项调用Java(例如java -Xmxn=...) - 我建议除非您在非常有限的硬件上运行,否则它不值得做。< / p>

但是,如果您确实希望每次处理一行时都避免分配String数组,那么有很多方法可以这样做。

一种方法是使用StringTokenizer

    StringTokenizer st = new StringTokenizer(line,"|");

    while (st.hasMoreElements()) {
        process(st.nextElement());
    }

您还可以避免一次消耗一条线。将您的文件作为流文件获取,使用StreamTokenizer,并以这种方式一次使用一个令牌。

阅读ScannerBufferedInputStreamReader的API文档 - 此区域有很多选择,因为您正在做一些基本的事情。

但是,这些都不会导致Java更快或更积极地使用GC。如果JRE认为自己没有内存,它就不会收集任何垃圾。

尝试写下这样的内容:

public static void main(String[] args) {
    Random r = new Random();
    Integer x;
    while(true) {
        x = Integer.valueof(r.nextInt());
    }
}

运行它并在运行时观察JVM的堆大小(如果使用率过快而无法查看,请暂停一下)。每次循环时,Java都会创建一个你称之为“临时对象”的对象。类型为Integer。所有这些都留在堆中,直到GC决定它需要清除它们。你会发现在达到一定程度之前它不会这样做。但是当它达到这个水平时,它将很好地确保永远不会超过它的限制。

答案 1 :(得分:1)

您应该调整分析情境的方式。虽然关于正则表达式编译的文章一般是正确的,但它并不适用于此。当您查看source code of String.split(String)时,您会看到它只是委托给String.split(String,int),它有一个特殊的代码路径,用于仅由一个文字字符组成的模式,包括像\|这样的转义字符

在该代码路径中创建的唯一临时对象是ArrayList。正则表达式包完全没有涉及;这个事实可能有助于您理解为什么预编译正则表达式模式并没有改善这里的性能。

当您使用Profiler得出对象太多的结论时,您还应该使用它来找出有哪些对象以及它们来自何处,而不是进行疯狂的猜测。

但目前尚不清楚,为什么你会抱怨。您可以将JVM配置为使用特定的最大内存。只要尚未达到该最大值,JVM就会按照您所说的内容执行操作,而不是仅仅为了不使用可用内存而浪费CPU周期。不使用可用内存的意义何在?