【问题标题】:String.split() temporary objects and Garbage CollectString.split() 临时对象和垃圾收集
【发布时间】:2016-08-28 16:17:52
【问题描述】:

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

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 的文件在文件处理结束时使用了大约 35 mb 的内存。

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

但它们似乎都没有足够的效率。使用 JProfiler,我可以看到临时对象使用的内存量太大(使用了 35 mb,但有效对象仅使用了 15 mb)。

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

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

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

【问题讨论】:

  • “split() 方法生成了太多没有被收集的临时对象(这里最好解释:chrononsystems.com/blog/…。” 太糟糕了,它没有解释什么你声称。目前还不清楚为什么要拆分字符串,而不是解析它。
  • 你接受或拒绝tokens的元素的标准是什么?
  • 显而易见的其他解决方案是不拆分字符串,而是原位扫描/解析/处理字符串。
  • 即使使用35mb,真的有关系吗?如果您的 JVM 没有那么多内存,它无论如何都会尝试在两者之间收集,如果有,那么为什么还要麻烦呢?最后它最终会收集。
  • 35 MB 和 16 MB 之间的差异大约值 10 美分。尝试节省 10 美分的内存需要多少时间?最低工资约为 1 分钟。一般来说,不要调用System.gc(),让JVM在需要的时候去做。

标签: java string performance split garbage-collection


【解决方案1】:

在这里要说的第一件事也是最重要的事情是,不要担心。 JVM 正在消耗 35MB 的 RAM,因为它的配置表明这是一个足够低的数量。当其高效的 GC 算法决定时间时,它会将所有这些对象扫掉,没问题。

如果您真的愿意,可以使用内存管理选项调用 Java(例如 java -Xmxn=...)——我建议除非您在非常有限的硬件上运行,否则不值得这样做。

但是,如果您真的想避免在每次处理一行时分配一个 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 决定需要清除它们。你会看到它不会这样做,直到它达到一定的水平。但是当它达到那个水平时,它会很好地确保永远不会超过它的限制。

【讨论】:

    【解决方案2】:

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

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

    当您使用 Profiler 得出对象过多的结论时,您还应该使用它来找出存在哪些类型的对象以及它们的来源,而不是胡乱猜测。

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

    【讨论】:

    • 感谢您的回答 Holger,但实际上,当我检查 Profiler 中可用的对象列表时,每次拆分后都有很多 Object[] 增长() 称呼。我尝试使用小文件进行测试,因为它很快(我可以运行 20 次测试并获得平均时间和内存使用情况)。另一件有趣的事:在这种情况下,我知道 32mb 应该足以处理文件,但是当我使用 -Xms16m -Xmx32m 运行测试时,它会导致“超出 GC 开销限制”
    • ArrayList 封装了一个 Object[] 实例,这并不奇怪。得到“超出 GC 开销限制”错误只是证明尝试不必要地限制内存会降低性能,正如该错误所说,垃圾收集花费了太多时间,请参阅stackoverflow.com/q/1393486/2711488
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-09
    • 2010-11-08
    • 2018-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-28
    相关资源
    最近更新 更多