G1垃圾收集器:永久代会不断填充直到进行完整GC。

30
我们有一个较大的应用程序在JBoss 7应用服务器上运行。过去,我们使用ParallelGC,但在一些堆很大(5 GB或更多)且通常接近填满的服务器上,我们经常会遇到非常长的GC暂停问题。
最近,我们改进了应用程序的内存使用情况,在一些运行应用程序的服务器上增加了更多的RAM,但我们也开始切换到G1,希望使这些暂停不那么频繁和/或时间更短。事情似乎有所改善,但我们看到了一种以前没有发生过的奇怪行为(使用ParallelGC时没有出现):Perm Gen似乎很快就会填满,一旦达到最大值,就会触发完整的GC,这通常会导致应用程序线程长时间暂停(在某些情况下,超过1分钟)。
我们已经使用512 MB的最大perm大小几个月了,在我们的分析中,使用ParallelGC时,perm大小通常会停止增长约在390 MB左右。然而,我们切换到G1后,上述行为开始发生。我尝试将最大perm大小增加到1 GB甚至1.5 GB,但仍然会发生完整的GC(只是不那么频繁)。
此链接中,您可以看到我们正在使用的性能分析工具(YourKit Java Profiler)的一些截图。请注意,当触发Full GC时,Eden和Old Gen有很多空闲空间,但Perm大小已达到最大值。 Full GC后,Perm大小和加载的类数急剧减少,但它们开始再次上升,循环重复。代码缓存很好,从未超过38 MB(在这种情况下为35 MB)。
以下是GC日志的一部分:
2013-11-28T11:15:57.774-0300: 64445.415: [Full GC 2126M->670M(5120M), 23.6325510 secs] [Eden: 4096.0K(234.0M)->0.0B(256.0M) Survivors: 22.0M->0.0B Heap: 2126.1M(5120.0M)->670.6M(5120.0M)] [Times: user=10.16 sys=0.59, real=23.64 secs]

您可以在这里查看完整的日志here(从我们启动服务器的时刻开始,到完全GC几分钟后)。

以下是一些环境信息:

java版本"1.7.0_45"

Java(TM) SE运行时环境(构建1.7.0_45-b18)

Java HotSpot(TM) 64位服务器VM(构建24.45-b08,混合模式)

启动选项:-Xms5g -Xmx5g -Xss256k -XX:PermSize=1500M -XX:MaxPermSize=1500M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -Xloggc:gc.log

所以,这里是我的问题:

  • 这是否是G1的预期行为?我在网上找到另一篇文章,有人质疑类永久代也应该执行增量收集,但没有得到答案...

  • 我们的启动参数中是否有可以改进/纠正的地方?服务器有8GB内存,但似乎硬件不缺乏,应用程序的性能良好,直到触发完整GC时,用户会遇到大延迟并开始抱怨。


我建议尝试添加“-verbose:gc”以查看更多详细信息,同时我可能会考虑尝试Chronon DVR(http://chrononsystems.com/)。 - Elliott Frisch
顺便说一下,我看了一下Chronon DVR,看起来很有趣,但我还需要再试用一下。然而,我不确定它是否能在这种情况下帮助我们... - Jose Otavio
@ElliottFrisch:我们确实使用了ParallelGC,但是我们遇到了不同种类的问题,这就是为什么我们决定尝试G1的原因。但这实际上是我的问题的一部分:G1是否适合我们?也许如果我们使用正确的参数,Concurrent Mark Sweep或Parallel GC可能会更好,但我仍然认为G1的这种行为非常奇怪,我想知道是否有人看到过这种情况,以及这是否是正常行为... - Jose Otavio
1
非常有趣的博客文章:http://mechanical-sympathy.blogspot.nl/2013/07/java-garbage-collection-distilled.html,总体上太长了,但最后一段概括得很好:“如果延迟峰值是由于GC引起的,则投资于调整CMS或G1,以查看是否可以满足您的延迟目标。有时这可能不可能,因为高分配和晋升率结合低延迟要求。 GC调优可能会成为一项高技能的练习,通常需要应用程序更改以减少对象分配率或对象寿命。” - smeaggie
1
Joshua Wilson的帖子涵盖了G1GC与CMS的一些好处,但是关于为什么会发生这种情况的答案可能在早期的电子邮件交流中,有趣的部分实际上从这里开始:http://mail.openjdk.java.net/pipermail/hotspot-gc-use/2010-July/000671.html。他们讨论了某些区域可能永远不会被收集,因此强制进行完整的GC。整个讨论中有一些非常有趣的指针,但不幸的是我在那里找不到明确的答案。也许您可以根据自己的代码经验找到一些指针。 - smeaggie
显示剩余23条评论
5个回答

33

增长Perm Gen的原因

  • 有很多类,特别是JSP。
  • 有很多静态变量。
  • 存在类加载器泄漏。

对于那些不知道的人来说,以下是一个简单的方式来理解PermGen填满的过程。Young Gen没有足够的时间来让对象过期,所以它们被移动到Old Gen空间。Perm Gen保存Young Gen和Old Gen中对象的类。当Young或Old Gen中的对象被收集并且该类不再被引用时,它将从PermGen中“卸载”。如果Young Gen和Old Gen没有被回收,那么Perm Gen也不会回收,一旦它填满了,就需要进行全停顿GC。更多信息请参见Presenting the Permanent Generation


切换到CMS

我知道你正在使用G1,但如果你切换到并发标记清除(CMS)低暂停时间垃圾收集器-XX:+UseConcMarkSweepGC,尝试通过添加-XX:+CMSClassUnloadingEnabled启用类卸载和永久代收集。


隐藏的陷阱

如果你正在使用JBoss,RMI/DGC的gcInterval设置为1分钟。RMI子系统每分钟强制进行一次完整的垃圾回收。这反过来会促使对象在Young Generation中晋升,而不是在其中被收集。

为了让GC能够进行适当的收集,你应该将其更改为至少1小时,如果可能的话改为24小时。

-Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000

所有JVM选项列表

要查看所有选项,请从命令行运行此命令。

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version
如果您想查看JBoss正在使用什么,则需要将以下内容添加到您的standalone.xml中。 您将获得每个JVM选项及其设置的列表。注意:它必须在您要查看的JVM中才能使用。如果您在外部运行它,您将无法看到JBoss正在运行的JVM中发生的情况。
set "JAVA_OPTS= -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal %JAVA_OPTS%"

当我们只对修改标志感兴趣时,有一个快捷方式可供使用。

-XX:+PrintcommandLineFlags

诊断

使用jmap来确定哪些类占用了永久代空间。输出结果将显示:

  • 类加载器
  • # 类数
  • 字节
  • 父类加载器
  • 存活/死亡
  • 类型
  • 总数

jmap -permstat JBOSS_PID  >& permstat.out

JVM选项

以下设置适用于我,但取决于您的系统设置和应用程序正在执行的操作,这将决定它们是否适合您。

  • -XX:SurvivorRatio=8 – 将幸存者空间比率设置为1:8,从而产生更大的幸存者空间(比率越小,空间越大)。 SurvivorRatio是伊甸空间与一个幸存者空间的大小比较值。更大的幸存者空间允许短寿命对象在年轻代中有更长的死亡时间。

  • -XX:TargetSurvivorRatio=90 – 允许占用90%的幸存者空间,而不是默认的50%,从而更好地利用幸存者空间内存。

  • -XX:MaxTenuringThreshold=31 – 防止过早地将对象从年轻代提升到老年代。允许短寿命对象在年轻代中有更长的死亡时间(因此,避免晋升)。该设置的一个后果是,由于要复制的额外对象数量增加,次要GC时间可能会增加。此值和幸存者空间大小可能需要进行调整,以便在幸存者空间之间复制的开销与寿命较长的对象之间进行平衡。CMS的默认设置为SurvivorRatio=1024和MaxTenuringThreshold=0,这会导致所有幸存者都被提升。这可能会对收集老年代的单个并发线程施加很大压力。注意:当与-XX:+UseBiasedLocking一起使用时,此设置应为15。

  • -XX:NewSize=768m – 允许指定初始年轻代大小

  • -XX:MaxNewSize=768m – 允许指定最大年轻代大小

这里是一个更详细的JVM选项列表。


1
我们确实尝试过G1,但后来又回到了UseConcMarkSweepGC并进行了这些更改。需要明确的是,这些不是随意的JVM设置,而是我们使用的设置。底部的注释旨在说明您只能使用其中一种选项,而不能同时使用,因为它们具有相同的功能。此外,我们遇到的问题不是晋升失败,而是晋升发生得太频繁。这会将本不应该到达的内容推入Perm空间。 - Joshua Wilson
1
您提供的额外信息很有帮助。在这里和其他地方阅读了很多之后,我们真的发现我们从错误的角度看待了问题:G1并不是问题的原因,它只是帮助暴露了问题。我们将首先解决加载太多类的问题(其中一个原因是我们的应用程序有许多调用远程EJB的调用,这些调用根本不需要远程),同时尝试使用不同的收集器和不同的参数来找到最佳的解决方案。 - Jose Otavio
Survivor和NewGen与PermGen并没有真正的关联(它们确实会影响保留的类,但只是短暂的)。 - eckes
@eckes - 那么你的意思是对象不会从新生代移动到老年代再到永久代吗? - Joshua Wilson
@Joshua 是的,这就是为什么我写道它们会影响保留的类。然而,在伊甸园中没有任何对象能够存活很长时间,因此通常不是导致PermGen泄漏的原因(只有“旧”代中的对象可以使类保持更长时间的生命)。 - eckes
显示剩余9条评论

2
这是G1的预期行为吗?我并不觉得惊讶。基本假设是放入permgen的东西几乎永远不会变成垃圾。因此,你可以期望permgen GC是"最后的手段";也就是说,只有在被迫进行完整GC时JVM才会这样做。(好吧,这个论点远远不能证明......但它与以下情况一致。)我看到很多其他收集器具有相同的行为;例如:
- permgen垃圾回收需要多次Full GC - Java GC出了什么问题?PermGen空间正在填充?
我发现另一个帖子,有人质疑类似的事情,并说G1应该对Perm Gen执行增量收集,但没有答案......我认为我找到了同样的帖子。但是,有人认为它"应该是"可能的并不是很有启示性。
在我们的启动参数中,有什么可以改进或更正的吗?
我表示怀疑。我的理解是,这是Permgen GC策略固有的问题。
我建议您要么找出并修复首先使用如此多Permgen的原因... 要么切换到Java 8,其中不再有Permgen堆:请参见PermGen elimination in JDK 8 虽然Permgen泄漏是一个可能的解释,但还有其他解释;例如:
- 过度使用`String.intern()` - 应用程序代码正在进行大量的动态类生成,例如使用`DynamicProxy` - 一个庞大的代码库......虽然这不会导致像您观察到的Permgen翻转那样的问题。

谢谢提供的信息。这确实是我们目前考虑采取的方式:找出导致加载如此多类的原因,然后调整我们的垃圾回收配置。正如我在另一个评论中所说,G1绝对不是问题的根本原因,但它帮助我们暴露了这个问题。 - Jose Otavio

1
我建议首先找到导致PermGen增大的根本原因,而不是随意尝试JVM选项。
  • 您可以启用类加载日志(-verbose:class,-XX:+ TraceClassLoading -XX:+ TraceClassUnloading,...),并查看输出
  • 在您的测试环境中,您可以尝试监视(通过JMX)何时加载类(java.lang:type = ClassLoading LoadedClassCount)。这可能会帮助您找出哪个部分的应用程序是负责的。
  • 您也可以尝试使用JVM工具列出所有类(抱歉,但我仍然主要使用jrockit,在那里您将使用jrcmd。希望Oracle已将这些有用的功能迁移到Hotspot...)
总之,找出生成如此多类的原因,然后考虑如何减少/调整垃圾回收。
祝好, Dimo

我认为你是对的,也许这是我们现在最好的选择。我开始觉得以前没有使用ParallelGC是因为主要收集更频繁,这可以防止Perm Gen过度增长。 - Jose Otavio
我们之前的思路有误,G1并不是问题的根本原因,只是帮助我们暴露了它。我们正在调查问题,并已经发现我们的应用程序存在大量远程EJB调用,因此我们首先解决这个问题。同时,我们将尝试使用不同的GC配置进行实验,直到找到最适合我们的方案。 - Jose Otavio

1

我同意上面的答案,你应该真正尝试找出填充permgen区域的是什么,我强烈怀疑这与你想找到根本原因的某个类加载器泄漏有关。

JBoss论坛中的此线程讨论了一些经过诊断的案例以及它们是如何被解决的。这个答案这篇文章也讨论了这个问题。在那篇文章中提到了可能是你可以进行的最简单的测试:

症状 当您重新部署应用程序而没有重新启动应用程序服务器时,这种情况才会发生。JBoss 4.0.x系列就遭受了这种类加载器泄漏的问题。因此,在JVM耗尽PermGen内存并崩溃之前,我最多只能重新部署我们的应用程序两次。
解决方案 为了确定这样的泄漏,请取消部署应用程序,然后触发完整的堆转储(在此之前请确保触发GC)。然后检查是否可以在dump中找到您的任何应用程序对象。如果是,则跟随它们到达其根,您将找到类加载器泄漏的原因。在JBoss 4.0的情况下,唯一的解决方案是每次重新部署都要重新启动。

如果您认为重新部署可能与此有关,那么我会首先尝试这个方法。这篇博客文章是早期的一篇文章,讨论了相同的事情,但也讨论了细节。根据帖子,可能并不是您实际上正在重新部署任何东西,而是PermGen自己填满了。在这种情况下,检查类+添加到PermGen的任何其他内容可能是解决方法(如先前答案中已经提到的)。

如果这不能提供更多见解,我的下一步将是尝试plumbr工具。他们有一种保证可以找到泄漏


我们通常不会在不重启 JBoss 的情况下重新部署应用程序,我真的认为现在 Perm Gen 很快就被填满的事实是由于我们应用程序的实现方式,G1 实际上只是帮助我们暴露了这个问题。 - Jose Otavio

-3

你应该使用带有-verbose:gc参数的java命令启动你的server.bat


1
如果使用-Xloggc和-XX:+PrintGCDetails,则无需使用-verbose:gc(它是一个遗留的同义词)。 - eckes

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