垃圾收集器占用了太多的CPU时间

6
我开发了一个处理大量数据并需要很长时间才能完成的Web应用程序。现在我正在对我的应用程序进行分析,并注意到GC的一个非常严重的问题。当出现Full GC时,它会停止所有进程30-40秒钟。我想知道是否有任何方法可以改善这种情况。我不想让CPU浪费太多时间在GC上。以下是一些有用的细节:
1.我使用的是Java 1.6.0.23。 2.我的应用程序最大内存为20 GB。 3.每14分钟会出现一次Full GC。 4.GC之前的内存为20 GB,GC后为7.8 GB。 5.CPU中使用的内存(即任务管理器中显示的)为41 GB。 6.进程完成后(JVM仍在运行),已使用内存为5 GB,可用内存为15 GB。

2
需要在问题中注明正在使用的特定 JVM 和内存选项(如果有)。还要包括任何分析结果以及它们是如何获得的。 - user166390
您可以定期建议在战略性选择的时间点运行GC,以进行更多持续时间较短的收集。 - Mister Smith
@pst 我已经添加了我观察到的大部分细节。 - NIVESH SENGAR
除非您告诉我们您使用的是哪个Java虚拟机,否则我们无法告诉您该怎么做。请尝试运行 java -version 命令。 - Aaron Digulla
@AaronDigulla 我已经在问题中添加了它。 - NIVESH SENGAR
5个回答

5
现代JVM使用许多垃圾收集算法。有些算法,如引用计数非常快,而有些算法,如内存复制非常慢。您可以更改代码以帮助JVM大多数时间使用更快的算法。
其中最快的算法之一是引用计数,正如名称所描述的那样,它计算对象的引用,并在达到零时准备进行垃圾回收,并在此之后减少当前GCed对象引用的对象的引用计数。
为了帮助JVM使用这种算法,请避免循环引用(对象A引用B,然后B引用C,C引用D....,Z再次引用A)。因为即使整个对象图不可达,也没有一个对象的引用计数器达到零。
当您不再需要循环中的对象时(通过将null分配给引用之一),只需打破循环即可。

1
虽然确实存在许多GC机制,其中引用计数是其中之一,但它早已被淘汰。当前的JVM使用了几种不同的技术来绕过您所识别的引用循环。第一个和最简单的是“活动副本”。这不会查看所有分配的对象。相反,它从线程关联的对象(根)开始,然后开始将所有可达的对象复制到新空间。完成后,它只释放剩下的任何东西。因此,任何没有从线程根路径的引用循环都会消失。 - chaotic3quilibrium
我知道这个(我称其为内存复制),但与引用计数相比,这是GC中最耗时的机制之一。因此,如果有人帮助GC使用引用计数机制而不是现场复制,就可以避免缓慢的现场复制机制。 - Amir Pashazadeh
我曾经也和你一样这么想。我会花些时间去回顾当前JVM GC的设计文档和讨论,它们是根据典型的垃圾生成模式进行设计的。虽然引用计数听起来很理想,但实际上只在非常独特和不寻常的情况下才能发挥作用。基本上,我必须扭曲我的设计,以避免任何参考路径中的任何循环。当然这是可能的。然而,在除了最简单的OO模型之外,它也极大地限制了我的设计(至少在我的世界里是这样)。 - chaotic3quilibrium

4
如果您使用64位架构,请添加:
-XX:+UseCompressedOops 将64位地址转换为32位
使用G1GC代替CMS:
-XX:+UseG1GC - 它使用增量步骤
将初始大小和最大大小设置相同:-Xms5g -Xmx5g
调整参数(仅示例):
-XX:MaxGCPauseMillis=100 -XX:GCPauseIntervalMillis=1000
请参见Java HotSpot VM Options 性能选项

4

要么通过重复使用资源来改进应用程序,要么在应用程序的某些关键区域自己触发System.gc()(这并不能保证有所帮助)。最有可能的是您的应用程序存在内存泄漏问题,您需要调查并重新构建代码。


2
自己调用System.gc()几乎总是一个坏主意。如果你依赖这个调用来提高性能,那么这表明代码存在问题。此外,它甚至不能保证做任何事情,也就是说,它只是一个请求,JVM可以完全忽略它。 - Black

3

垃圾回收所需的时间取决于两个因素:

  • 有多少对象是活动的(即可以从任何地方访问)
  • 有多少无用对象实现了finalize()

在Java中,那些无法被访问且不使用finalize()的对象清理起来不需要花费任何代价,这就是为什么Java通常与其他语言(如C++)相当(并且通常更好,因为C++花费大量时间来删除对象)。

因此,在应用程序中,您需要减少存活对象的数量和/或在代码中尽早切断对不再需要的对象的引用。例如:

当您有一个非常长的方法时,您将保留从局部变量引用的所有对象。如果您将该方法拆分为许多较小的方法,则引用将更快地丢失,垃圾回收器将不必处理这些对象。

如果您将您可能需要的一切都放在巨大的哈希映射中,那么这些映射将使所有这些实例保持活动状态,直到您的代码完成。因此,即使您不再需要它们,垃圾回收器仍将不得不花费时间处理它们。


谢谢Aaron,你在这里提到的可能是我的应用程序中的问题。因为有一些庞大的方法,我正在使用许多Maps和Lists来缓存数据。但是我必须缓存数据,因为我经常使用它们。 - NIVESH SENGAR
那么,即使没有任何对象变成死亡状态,GC 时间会随着存活对象数量的增加而变得越来越糟糕吗? - weston
@weston 简短的回答是“是”。更长的回答是“取决于情况”。当活动对象的体积超过总可用空间的80%时,GC会变得更加积极。这是为了防止可能出现的内存不足问题。因此,在需要具有长期生存对象的系统上,使用其他类型的缓存策略;这些策略的内存消耗要少得多,但访问数据的代价是速度较慢。 - chaotic3quilibrium
@chaotic3quilibrium 嗯,我不知道这个人的系统限制是什么,但他的应用程序正在使用20GB,所以除非他有一个32GB的怪兽服务器,否则我想它将处于激进模式! - weston
1
@AaronDigulla 我现在明白了,短生命周期的对象很容易清理。如果有人对它的工作原理感兴趣,我想分享这个链接。http://www.ibm.com/developerworks/java/library/j-jtp11253/ (您的JVM GC可能会有所不同)。 - weston
显示剩余2条评论

3

不要过度使用new,可以减少需要收集的内容。

假设您有A类。您可以在其中包括对A类的另一个实例的引用。这样您就可以创建A类实例的“自由列表”。每当您需要A时,只需从自由列表中弹出一个即可。如果自由列表为空,则使用new创建一个。

当您不再需要它时,将其推送到自由列表中。

这可以节省很多时间。


2
我已经阅读过,“对象池”在除了那些构造成本昂贵的对象之外,可能是一种反模式。请参见这里:http://www.ibm.com/developerworks/java/library/j-jtp01274/index.html 中的“对象池”部分。 - weston
@weston:只有在手动堆栈采样证明分配和释放占用了大部分时间后,我才会这样做。此外,人们写他们想写的东西,所以如果有人说某个东西是反模式,并不意味着它就是。保留自己的判断是好的。 - Mike Dunlavey
new 几乎不会对 GC 时间产生影响。您可以创建一个包含数百万个对象的列表(这需要一些时间),然后将引用设置为 null - 当下次 GC 运行时,它所需的时间与从未创建列表时相同。无法访问的对象不会对 GC 时间产生影响。 - Aaron Digulla
@AaronDigulla:为什么呢?你的意思是我可以创建一个巨大的内存泄漏,而垃圾回收甚至不会有丝毫影响吗?我猜那真是太神奇了 :) - Mike Dunlavey
好的,那个人并没有说这是一个坏主意,但他认为应该保留它在确实有实际好处的时候使用,这正是你现在所说的。顺便说一句,很高兴看到你和@AaronDigulla正在讨论这个问题,因为你们似乎对此有完全相反的观点。一方面认为更多的对象=较慢的GC,而另一方面则认为保留对象可以改善GC! - weston
显示剩余5条评论

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