GC调优-预防Full GC

25
我正在尝试避免在生产中运行Tomcat上的 Grails 应用程序时发生 Full GC(从下面的 gc.log 样本中)。 有没有关于如何更好地配置GC的建议?
14359.317:[Full GC 14359.317:[CMS:3453285K->3099828K(4194304K),13.1778420秒]4506618K->3099828K(6081792K),[CMS Perm:261951K->181304K(264372K)] icms_dc = 0,13.1786310秒] [ Times:user = 13.15 sys = 0.04,real = 13.18秒]How can I improve my memory?
There are several ways to improve memory:
1. Pay attention and focus on what you're trying to remember. 2. Use repetition and rehearsal techniques to reinforce memories. 3. Create associations or connections between new information and existing knowledge. 4. Break up complex information into smaller, more manageable pieces. 5. Get enough sleep and exercise regularly to support brain function. 6. Use mnemonics or memory aids, such as acronyms or visual imagery. 7. Practice retrieval of information through self-testing or quizzes. 8. Minimize distractions and avoid multitasking when learning new material. 9. Maintain a healthy diet and stay hydrated. 10. Consider using memory-enhancing supplements or techniques, but consult with a healthcare professional first.

MaxPermSize=1G和"-Dsun.reflect.inflationThreshold=0"与另一个问题相关,我更愿意保持分开。

"-XX:+CMSClassUnloadingEnabled"和"-XX:+CMSPermGenSweepingEnabled"是因为Grails需要大量的额外类来支持闭包和反射。

-XX:+CMSIncrementalMode是一个实验,但并未取得太大成功。


你有多少个处理器? - DNA
运行jdk1.6.0_07,16个处理器,32GB的内存。 - Kalisen
你为什么选择使用这些开关? - Matt
我旨在实现的策略(更多细节见上文): 我希望尽量减少Tenured存储的对象数量,因为我正在处理请求,并且预计除了某些共享对象之外,其他对象仅对当前请求有用。因此,通过使用较大的NewSize和增加的TenuringThreshold,并希望没有这些仅用于单个请求的对象会留下来。 - Kalisen
5个回答

11

您发布的日志片段显示您有大量对象存活时间超过320秒(约为每个年轻集合40秒,并且对象在晋升之前经历了8次集合)。剩余的对象然后流入老年代,最终会出现一个明显意外的full gc,实际上并没有收集很多。

3453285K->3099828K(4194304K)

也就是说,当触发时,您拥有一个4G的老年代,其空间使用率为约82%(3453285/4194304),并且在13秒后使用率约为74%。

这意味着需要13秒才能收集总共约350M的内存,在6G堆上,这并不算太多。

这基本上意味着您的堆大小不足,或者更可能的是存在内存泄漏。对于CMS来说,这样的泄漏是一件可怕的事情,因为并发老年代集合是非压缩事件,这意味着老年代是由空闲列表组成的集合,这意味着片段化可能会成为CMS的一个重要问题,这意味着您对老年代的利用效率变得越来越低效,这也意味着促进失败事件的概率增加(虽然如果这是这样的事件,则我希望看到一个日志消息)。因为它想要将(或认为将需要)X MB晋升到老年代,但它没有可用的(连续的)大于等于X MB的空闲列表。这会触发一个意外的老年代集合,这是一个完全非并发的STW事件。如果您实际上要收集的很少(就像您一样),那么您无法惊讶地坐在那里闲着。

以下是一些常规指针,在很大程度上重申了Vladimir Sitnitov所说的内容...

  • 在多核框中使用iCMS毫无意义(除非您有大量的JVM或其他进程在运行该框,以便JVM确实缺少CPU),因此请删除此开关
  • 由于在每次集合时在幸存区之间复制相当大量的内存的影响,您的年轻集合时间过长,150-200ms是一个真正巨大的ParNew集合
  • 针对年轻代的问题,正确的解决方案取决于分配行为的实际情况(例如,您可能更好地提前tenuring并减少在老年代集合上碎片的影响,或者您可以拥有一个更大的新生代,并减少年轻代集合的频率,以使较少的对象被晋升,从而最小化深入老年代的现象)。

一些问题...

  • 它最终是否会耗尽内存或恢复正常?
  • 在此日志片段期间,应用程序是否处于稳定状态(在启动后的某个时刻受到一致的负载),还是正在遭受压力?

我的主要目标是避免长达10秒以上的阻塞GC,我相信只要它是并行的,我可以接受一定量的GC噪音。但我想进行更多的测试以确保这一点。它总是会恢复的。负载在一天中变化,持续几个小时的尖峰。 - Kalisen
你需要做的是确定那些对象(渗入 tenured 区域的)实际上存在多长时间?你还需要弄清楚两代对象之间是否存在重要联系?你目前的策略似乎是合理的,但如果堆大小没有显著增加,它可能是站不住脚的。如果你可以升级以获取 compressedoops,那么一定要这样做(因为没有它的 6G 堆实际上更像是一个 3-4G 的堆),无论如何,你都应该考虑增加堆大小,以便你可以了解稳态看起来真正是什么样子。 - Matt

8
我正在处理请求,并期望除了一定数量的共享对象,其他对象只对当前请求有用。这是理论,但任何类型的缓存都可以轻松地使该假设无效,并创建超出请求范围的对象。
正如其他人所指出的那样,你的巨大年轻一代和扩展寿命似乎都没有起作用。您应该分析应用程序的剖面和对象的年龄分布。我很确定Grails会缓存所有种类的东西,超出了请求的范围,这就泄漏到了老年代中。
你实际上正在尝试牺牲年轻代暂停时间(对于2GB的年轻代)来推迟不可避免的6GB老年代收集。这并不是一个好的权衡。
相反,您可能应该以更好的年轻代暂停时间为目标,并允许CMS烧掉更多的CPU时间:更多的并发阶段GC线程(记不得该选项),更高的GCTimeRatio,MaxGCPauseMillis> MaxGCMinorPauseMillis,以减轻次要集合的压力,并允许它们达到暂停目标,而不必调整大小以适应主要集合限制。
为了使主要GC变得不那么痛苦,您可能需要阅读此文档:http://blog.ragozin.info/2012/03/secret-hotspot-option-improving-gc.html(此补丁应该在j7u4中)。CMSParallelRemarkEnabled也应启用,不确定是否为默认设置。
另一种选择:使用G1GC
就我个人而言,我对G1GC的一些可怕经历是由于某些非常大的LRU样式工作负载而自己陷入困境,然后比CMS更经常地回退到一个大的停机收集。但对于其他工作负载(如您的工作负载),它实际上可能会执行任务并递增地收集旧一代,同时还进行压缩,从而避免任何大暂停。
如果您还没有尝试过,请试试。再次更新到最新的java7之前,因为G1仍然存在一些问题,他们正在努力解决这些问题。
另一种选择:吞吐量收集器
由于您已经在2GB年轻代上使用并行收集器并且可以摆脱200ms的暂停时间...为什么不在6G堆上尝试并行老年代收集器呢?这可能需要比CMS看到的10秒+主要集合少。每当CMS遇到其故障模式之一时,它会进行单线程的停机收集。

4
请说明Tomcat可以使用多少个CPU? 4个?
您正在使用哪个Java版本?(>1.6.0u23?)
从完整的GC输出来看,似乎您已经达到了内存限制:即使进行了全面的GC,仍然有3099828K的已用内存(总共4194304K)。 当您没有内存时,就没有办法防止Full GC。
您的应用程序预计需要3.1GB的工作集吗? 那是3.1GB的非垃圾内存!
如果是预期的,那么现在是时候增加-Xmx/-Xms了。 否则,现在是时候收集和分析堆转储以识别内存占用情况了。
在解决了3GB工作集的问题之后,您可能会发现以下建议很有用: 在我看来,常规(非增量)CMS模式和减少NewSize值值得一试。
增量模式针对单CPU机器,当CMS线程将CPU让给其他线程时。 如果您有一些闲置的CPU(例如,您正在运行多核机器),最好在后台执行GC而不进行让步。
因此,我建议删除-XX:+CMSIncrementalMode。
-XX:CMSInitiatingOccupancyFraction=60告诉CMS在OLD gen填满60%后开始后台GC。
如果堆中有垃圾,并且CMS无法跟上它,降低CMSInitiatingOccupancyFraction是有意义的。 例如,-XX:CMSInitiatingOccupancyFraction=30,因此CMS会在旧代填满30%时开始并发收集。 目前很难说这是否是情况,因为您的堆中没有垃圾。
看起来“扩展保留”没有帮助-即使经过7-8个保留期,对象仍然不会死亡。我建议减少SurvivorRatio(例如,SurvivorRatio=2,或者只需删除该选项并使用默认值)。 这将减少保留期的数量,从而减少次要GC暂停。
-XX:NewSize=2G。您尝试过更低的NewSize值吗? 例如,NewSize=512m。这应该会减少次要GC暂停,并使晋升年轻->旧的更少,简化CMS的工作。

运行jdk1.6.0_07,16个处理器,32GB的RAM。 关于IncrementalMode的观点已被采纳。我使用-XX:CMSInitiatingOccupancyFraction的目标是确保在整个NewGen被提升的情况下,Old Gen中有足够的空间。对于3)的观点已被采纳。我开始时NewSize=512m,但考虑到对象创建的速率,增加NewGen的影响很小。我尝试了1G,显然减少了tenuring的数量,所以我将其推到2G,以查看趋势是否继续支持我的策略。 - Kalisen
2
你试过升级到1.6.0u26或更高版本吗?它默认使用(自u23以来)compressedoops,因此由于64位指针而浪费的内存会更少。 - Vladimir Sitnikov

3
你的幸存者大小几乎没有减少,如果有减少的话应该急剧减少,因为你只希望少数对象存活足够长的时间到达旧的一代。这表明许多对象存在相对较长的时间——例如当你有许多未能及时处理的打开连接、线程等情况时可能会发生。 顺便问一下,你是否有改变应用程序的选项,或者你只能修改GC设置?可能也会有Tomcat设置会产生影响...

有并行工作来查找明显的垃圾来源。我正在尝试与当前应用程序一起工作。 - Kalisen

0

以下是我对4核Linux服务器的设置。

根据我的经验,您可以调整-XX:NewSize -XX:MaxNewSize -XX:GCTimeRatio参数,以实现高吞吐量和低延迟。

-server
-Xms2048m
-Xmx2048m
-Dsun.rmi.dgc.client.gcInterval=86400000
-Dsun.rmi.dgc.server.gcInterval=86400000
-XX:+AggressiveOpts
-XX:GCTimeRatio=20
-XX:+UseParNewGC
-XX:ParallelGCThreads=4
-XX:+CMSParallelRemarkEnabled
-XX:ParallelCMSThreads=2
-XX:+CMSScavengeBeforeRemark
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=50
-XX:NewSize=512m
-XX:MaxNewSize=512m
-XX:PermSize=256m
-XX:MaxPermSize=256m
-XX:SurvivorRatio=90
-XX:TargetSurvivorRatio=90
-XX:MaxTenuringThreshold=15
-XX:MaxGCMinorPauseMillis=1
-XX:MaxGCPauseMillis=5
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution
-Xloggc:./logs/gc.log


我已经删除了您网站的链接;它似乎没有包含任何与此处提出的问题相关的信息。添加它会使您的帖子看起来像垃圾邮件。 - Andrew Barber

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