减少大量字符串的垃圾收集器开销

3
我正在构建一个包含百万行的电子表格,其中任何在此过程中完成的操作,都会以百万倍的速度累加。我遇到的问题之一是,在处理单元格中的公式时,我必须解析公式,调整引用,然后重新构建公式。在此过程中,我创建了5-12个字符串(取决于在标记化时有多少对象),然后就不再使用。
我发现垃圾收集器在这个处理过程中占用了70%的时间,并且创建的主要对象随后被收集以释放内存的对象就是这些字符串。
是否有任何方法减少GC(垃圾回收)带来的影响? (如果这是C ++,我会创建一个字符串池以重复使用。)
细节:
这是一个报告程序。我们读取模板,合并数据以生成最终报告,对最终报告进行处理,然后将其写入磁盘。报告作为文档对象保存,在这种情况下,99%是单个表,具有100万行(当所有数据合并时),每行有6个单元格,每个单元格可选:一个公式,一个值和/或格式化文本正文。
在处理过程中,有大量为短时间使用而创建的字符串。它让我困扰的情况是单元格公式的调整。模板中有几个单元格具有公式,例如“=A5 + A6”,然后根据每一行的位置进行调整。我解析出{" A5","+"," A6"}这些对象,然后为它们各自所在的行进行调整,在一个StringBuilder中将这些东西放回到一起,并使用toString()将其分配回单元格中的公式字符串对象。
大多数文档对象写入磁盘的困难之处在于文档对象不是从中读取,操作并写出新文档对象的。为了减少内存占用并处理需要沿列而不是行行走的情况,我们按原样调整对象。问题是当我们的内存不足时 - 整个过程运行得非常快,直到达到该点。我正在使用YourKit来对性能进行分析,并且收集String对象的情况很劣。传递StringBuilder对象可以在一定程度上帮助,但效果并不太大,因为我将会收集很多这样的对象(较少,但仍然很多)。

尝试使用StringBuilder/StringBuffer代替String怎么样? - Juned Ahsan
1
我敢打赌你在往错误的方向努力。每秒分配6M个字符串对于GC来说微不足道。内存不足是一个问题。请看我的答案。垃圾回收需要执行的工作量与丢弃的对象数量不成比例 - maaartinus
2个回答

3
我认为这与处理数百万个字符串无关。我刚刚测试过,我可以以持续的速度创建600万个字符串每秒,并且垃圾回收非常空闲。
问题似乎是你的内存不足。这使得垃圾回收更频繁地工作,并且更难以保持程序运行。
因此,不要浪费时间尝试减少分配速率。
获得更多内存或减少消耗通常是最便宜的方法。要减少内存消耗,请考虑:
- 处理数据,使您不需要同时将所有内容存储在内存中 - 打包相同的数据。一个字符占用2个字节,这意味着浪费了一半的内存(假设您主要使用ASCII)。 - 对某些字符串进行整理。也许有很多短字符串重复出现很多次... - 使用堆外内存。
没有您的程序,很难说更多。
使用-XX:+PrintGCDetails-XX:+PrintGCTimeStamps。这就是我得到的结果 - 几乎没有GC开销
10.075: [GC [PSYoungGen: 442272K->896K(425472K)] 442852K->1476K(769024K), 0.0016600 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
10.323: [GC [PSYoungGen: 425344K->928K(409600K)] 425924K->1508K(753152K), 0.0017150 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
10.558: [GC [PSYoungGen: 409504K->928K(394240K)] 410084K->1508K(737792K), 0.0014760 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
10.791: [GC [PSYoungGen: 394144K->928K(379904K)] 394724K->1508K(723456K), 0.0017070 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

是的,当内存不足时,这就会变得非常糟糕。但问题在于我只需要这些字符串暂时使用,如果有一种方法可以减少所有这些短期使用所带来的GC负担,那么这个问题就解决了。 - David Thielen
另外,我也考虑过将写入部分存储到磁盘上,但基于内存中数据的访问方式,这并不容易。 - David Thielen
请给我们提供更多细节。你的数据结构是什么样子的?你知道如何调整垃圾回收器吗?你主要使用 ASCII 吗? - maaartinus
添加了更多细节。也许我们需要在处理过程中将大部分文档写入磁盘,但这并不美观,也不容易。 - David Thielen
不幸的是这不是我们的电脑问题。我们是许多公司使用的商业库。有些公司正在转向绝对巨大的报告,因此我们需要尽力而为。我认为使用CharSequence创建自己的StringBuilder可能是最好的选择,就像你说的那样。有时候我会想念C++... - David Thielen
显示剩余3条评论

2
内存中的对象过多意味着Garbage Collector需要进行太多的清理工作。如果你使用StringBuilder/StringBuffer代替String,相信你可以减少创建对象的数量。每次操作一个String对象时,由于其不可变性质,都会创建一个新的对象。
但是,如果你使用StringBuilder/StringBuffer来操作字符串,就不会创建新的对象。StringBuilder/StringBuffer具有动态调整大小的功能,但如果你适当选择StringBuilder/StringBuffer的初始大小,也可以限制其大小。
简而言之,对象越少,Garbage Collector需要的工作量就越小。

1
基本上来说,这是错误的。 "内存中有太多未使用的对象,意味着垃圾收集器需要进行大量的清理工作。" 未使用(不可访问)的对象对于GC来说是“便宜”的。昂贵的是要处理的可达对象。 - Stephen C
是我...我完全不同意这种优化方式。特别是我不喜欢你的最后一句话。并不是像垃圾回收器必须把死对象埋葬掉那样。快速死亡的对象是相当便宜的,它们不能造成如此大的开销。 - maaartinus
1
如果OP的程序在GC中花费了70%的时间,最有可能的原因是堆不够大。 (或者是存储泄漏,导致堆被填满了无用的可达对象。同样的区别...)第二个最有可能的原因是GC调优参数选择不当。 - Stephen C
我同意将“未使用”这个误导性用法移除,但我仍然认为如果堆大小不能满足这些对象的需求,会有更多的内存对象使GC忙碌。由于没有提供代码,我们无法确定这些对象的范围/生命周期。我的答案基于一些假设,但我不认为是错误的。 - Juned Ahsan
@StephenC - 当我剩下很少的空闲内存并且需要短时间内创建大量小对象(其中很多是字符串)时,您有什么关于GC调优参数的建议吗? - David Thielen
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接