如何减少Java并发模式失败和过度GC

45

在Java中,并发模式失败是指并发收集器未能从tenured和permanent gen中释放足够的内存空间,不得不放弃并让完全的停止-全局垃圾回收启动。最终结果可能非常昂贵。

我理解这个概念,但从未对以下两点有一个良好的全面了解:
A)什么可能导致并发模式失败?
B)解决方案是什么?

这种不清晰会导致我编写/调试代码时没有很多提示,并且经常不需要特定原因就不得不尝试从foo到bar使用那些性能标志。

我想向开发人员学习你们的经验。如果您遇到此类性能问题,原因是什么以及如何解决它?

如果您有编码建议,请不要太笼统。谢谢!


你是否真正经历过并发模式失败? - Justin
4
老实说,我相当经常遇到这种情况。例如:295285.052秒时进行一次垃圾回收,其中新生代内存使用量为197658K,总大小为245760K;CMS阶段发生了并发模式失败,堆内存使用量从936976K变为904898K,总大小为962560K,此次垃圾回收持续时间约为5.3725960秒。有时候会很快出现OOM并被强制终止,有时则会经历长时间的GC周期(上次持续了超过10个小时)。 - jimx
你在启动Java时添加了一些GC设置吗?如果是的话,那些设置是什么? - Inv3r53
1
尝试了相当多的组合。例如,这是我当前的设置:-Xmx1240m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -Xloggc:/tmp/gc.log -XX:NewSize=300m -XX:MaxNewSize=300m -XX:SurvivorRatio=3 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/java.heapdump.hprof -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:OnOutOfMemoryError="kill -9 %p" -XX:MaxPermSize=128m关键是我不想深入研究我的具体情况。我也尝试过其他设置。 - jimx
总的来说,我的以往经验并不是很成功。我想学习更多真实世界的案例,并看看其他人如何解决Java性能问题。 - jimx
你正在使用JDK 1.5吗?并行现在是默认设置,所以你选择CMS有什么特别的原因吗?实际上,我们正处于类似的情况中,一个表现良好的CMS JVM(1.4.2)现在已经崩溃了,每周有300个Full GC。我们仍在调查中。 - JoseK
4个回答

24
我学习到的关于CMS的第一件事是,它需要比其他收集器更多的内存,增加25%到50%是一个好的起点。这有助于避免碎片化,因为CMS不像停止世界(stop the world)收集器那样进行任何压实操作。其次,要做一些有助于垃圾收集器的事情:使用Integer.valueOf而不是new Integer,摆脱匿名类,确保内部类不会访问无法访问的内容(外部类中的private等)。垃圾越少越好。在这方面,FindBugs和不忽略警告将对此有很大帮助。
至于调优,我发现你需要尝试几件事情:
-XX:+UseConcMarkSweepGC 告诉JVM在旧生代中使用CMS。
固定堆的大小:-Xmx2048m -Xms2048m 这可以防止GC做像扩大和缩小堆之类的事情。
-XX:+UseParNewGC 在年轻代中使用并行(parallel)而不是串行(serial)的收集。特别是如果你配置了非常大的年轻代,则这将加快你的小型收集。大的年轻代通常很好,但不要超过老年代大小的一半。
-XX:ParallelCMSThreads=X 设置CMS在并行执行操作时使用的线程数。
-XX:+CMSParallelRemarkEnabled 默认情况下remark是串行的,这可以加速您的程序。
-XX:+CMSIncrementalMode 允许应用程序在阶段之间暂停GC以更多地运行。
-XX:+CMSIncrementalPacing 允许JVM随时间推移更改收集频率。
-XX:CMSIncrementalDutyCycleMin = X 做GC的最小时间量。
-XX:CMSIncrementalDutyCycle = X 开始做GC的百分比时间。
-XX:CMSIncrementalSafetyFactor = X 我发现,如果设置它基本上始终在进行收集,可以获得通常较低的暂停时间。由于大部分工作都是并行完成的,因此你最终会得到基本上是规律可预测的暂停。

-XX:CMSFullGCsBeforeCompaction=1

这个很重要,它告诉CMS收集器在开始新一轮垃圾收集之前必须完成上一轮的收集。如果没有这个设置,可能会出现把很多工作都丢掉然后重新开始的情况。

-XX:+CMSClassUnloadingEnabled

CMS默认会让PermGen区域持续增长,直到几周后杀死你的应用程序。这个选项可以停止PermGen的增长。如果您使用了反射(Reflection),或者滥用String.intern,或者使用类加载器做一些不好的事情,那么PermGen可能会不断增长。

根据对象的生命周期长短以及在survivor空间之间进行的对象复制量,也可以调整Survivor比率和tenuring阈值。如果你知道所有的对象都会保留下来,可以配置零大小的survivor空间,任何在young gen收集中幸存下来的对象都将立即进入老年代。


12

引用自“理解并发标记扫描垃圾收集器日志”

通过增加老年代大小或设置CMSInitiatingOccupancyFraction为较低值,在更少的堆占用情况下启动CMS收集,可以避免并发模式失败。

但是,如果您的应用程序确实存在内存泄漏,那么您只是在拖延时间。

如果您需要快速重启和恢复,并且更喜欢“快速死亡”方法,请不要使用CMS。我建议使用'-XX:+UseParallelGC'。

“垃圾回收器人体工程学”

并行垃圾收集器(UseParallelGC)如果花费过多时间来收集少量堆,则会抛出内存不足异常。为了避免这种异常,您可以增加堆的大小。您还可以设置参数-XX:GCTimeLimit = time-limit-XX:GCHeapFreeLimit = space-limit


我尝试过使用 CMSInitiatingOccupancyFraction。听起来在我们的情况下可能不是个好主意。我宁愿购买快速内存条。 - jimx
虽然我还不确定是否要立即切换到ParallelGC。我们以前的经验表明,长时间暂停主要是由于CMS失败和全gc启动引起的。那些连续的全gc才是罪魁祸首。我试图找出是否有一种方法可以保持在CMS中,但当过多的全gc时间太长时,将其杀死。GCTimeLimit和GCHeapFreeLimit在CMS下仍然有效吗? - jimx
我猜GCTimeLimit和GCHeapFreeLimit只是用于并行算法。在发现内存泄漏之前,我们通常会在与用户协商的时间窗口内周期性地重置JVM以保持正常状态。 - fglez
第一个链接已经失效。这里有一个快照:http://web.archive.org/web/20100215134334/http://www.sun.com/bigadmin/content/submitted/cms_gc_logs.jsp - qingbo

4
有时OOM很快就被杀死了,有时会经历长时间的gc周期(上次超过10小时)。这听起来像是内存泄漏导致的问题。
CMS失败不会(据我所知)引起OOM。相反,CMS失败发生是因为JVM需要太多的集合太快,而CMS无法跟上。在堆接近满的情况下,一个短时间内发生许多收集周期的情况是其中之一。
真正长的GC时间听起来很奇怪...但如果你的机器非常糟糕地抖动,理论上是可能的。然而,如果你的堆非常接近满,重复GC的长时间是相当可信的。
如果你还没有这样做,你可以配置GC在堆达到最大大小并且在完成全GC后仍然接近满时放弃。尝试这样做。这不会治愈你的问题,但至少你的JVM会更快地得到OOM,允许更快的服务重启和恢复。 编辑 - 这个选项是 -XX:GCHeapFreeLimit=nnn,其中nnn是0到100之间的数字,表示在GC后必须保留的最小堆空闲百分比。默认值为2。该选项列在名为"Java 6 JVM中最完整的-XX选项列表"的页面中。(该页面列出了许多未在Sun文档中出现的-XX选项。不幸的是,该页面对这些选项的实际作用提供的细节很少。)
你应该开始查看你的应用程序/网站是否存在内存泄漏。如果有,除非找到并修复这些泄漏,否则问题不会消失。从长远来看,调整Hotspot GC选项无法解决内存泄漏问题。

明白了。我知道我们的程序存在慢性泄漏问题,但我们还没有找到它的根源。 :( 同时,我们正在尝试尽可能地挤压程序,看看不同的垃圾回收策略是否有助于缓解这个问题。CMS 不会直接导致 OOM,但通常会在进行完整的 gc 时出现严重问题。当我们的 gc 日志中出现 CMS 模式失败时,我们经常看到性能开始下降。但也许是由于缺乏经验,我们还没有找到泄漏的原因或找到适合的垃圾回收策略。 - jimx
我感觉堆满并不一定意味着会有严重的垃圾回收问题,但是在完整的垃圾回收后无法收集太多垃圾是一个非常糟糕的迹象。如果我想要让jvm尽早快速地杀死应用程序,我应该使用什么标志?更快速的服务重启和恢复听起来很有前途。至少我们不必忍受长时间的无响应。我宁愿选择快速死亡。谢谢。 - jimx

0

我发现使用-XX:PretenureSizeThreshold=1m可以使“大”对象立即进入老年代空间,从而极大地减少了我的young GC和并发模式失败,因为它倾向于在完整的CMS周期完成之前不尝试转储young + 1 survivor数据量(xmn=1536m survivorratio=3 maxTenuringThreashould=5)。是的,我的survivor空间很大,但每2天左右就会有一些需要它的应用程序(我们每天运行12个应用服务器)。


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