G1垃圾收集器:Perm-Gen无限期地填充,直到执行Full-GC



我们有一个相当大的应用程序在JBoss 7应用程序服务器上运行。过去,我们使用ParallelGC,但在一些堆很大(5GB或更大)且通常几乎已满的服务器中,它给我们带来了麻烦,我们经常会得到很长的GC暂停。

最近,我们改进了应用程序的内存使用情况,在少数情况下,我们为运行应用程序的一些服务器添加了更多的RAM,但我们也开始切换到G1,希望减少这些暂停的频率和/或缩短暂停时间。情况似乎有所改善,但我们看到了一种以前从未发生过的奇怪行为(使用ParallelGC):Perm-Gen似乎很快就会充满,一旦达到最大值,就会触发Full-GC,这通常会导致应用程序线程长时间暂停(在某些情况下,超过1分钟)。

几个月来,我们一直在使用512 MB的最大perm大小,在我们的分析过程中,使用ParallelGC,perm大小通常会在390 MB左右停止增长。然而,在我们切换到G1之后,上面的行为开始发生。我试着将最大烫发大小增加到1 GB,甚至1.5 GB,但全GC仍在发生(只是频率较低)。

在这个链接中,您可以看到我们正在使用的评测工具(YourKit Java Profiler)的一些屏幕截图。请注意,当Full GC被触发时,Eden和Old Gen有很多可用空间,但Perm大小是最大的。完整GC之后,Perm大小和加载的类的数量急剧减少,但它们又开始上升,并且循环重复。代码缓存很好,永远不会超过38MB(在这种情况下是35MB)。

以下是GC日志的一段:

2013-11-28T11:15:57.774-0300:64445.415:[完整GC 2126M->670M(5120M),23.6325510秒][伊甸园:4096.0K(234.0M)->0.0B(256.0M)幸存者:22.0M->0.0B堆:2126.1M(5120.0M)->670.6M(5120.0.M)][时间:用户=10.16系统=0.59,实际=23.64秒]

您可以在这里看到完整的日志(从我们启动服务器的那一刻起,直到GC完成后的几分钟)。

以下是一些环境信息:

java版本"1.7.0_45"

Java(TM)SE运行时环境(内部版本1.7.0_45-b18)

Java HotSpot(TM)64位服务器虚拟机(内部版本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的预期行为吗?我在网上发现了另一篇帖子,有人质疑类似的事情,说G1应该在Perm Gen上执行增量收集,但没有答案。。。

  • 在我们的启动参数中,有什么我可以改进/纠正的吗?服务器有8GB的RAM,但我们似乎并不缺乏硬件,在触发完整的GC之前,应用程序的性能很好,这时用户会遇到很大的滞后并开始抱怨。

Perm Gen增长的原因

  • 很多类,尤其是JSP
  • 有很多静态变量
  • 存在类加载程序泄漏

对于那些不知道的人,这里有一个简单的方法来思考PremGen是如何填充的。年轻一代没有足够的时间让事情过期,所以他们被转移到了老一代的空间。Perm Gen保存Young和Old Gen中对象的类。当Young或Old Gen的对象被收集,并且该类不再被引用时,它就会从Perm Gen中"卸载"。如果Young和Old Gen没有获得GC,那么Perm Gen也没有,一旦它填满,它就需要一个全停世界GC。有关更多信息,请参阅介绍永久一代。


切换到CMS

我知道您使用的是G1,但如果您切换到并发标记扫描(CMS)低暂停收集器-XX:+UseConcMarkSweepGC,请尝试通过添加-XX:+CMSClassUnloadingEnabled来启用类卸载和永久生成集合。


隐藏的Gotcha’

如果使用JBoss,RMI/DGC会将gcInterval设置为1分钟。RMI子系统会强制每分钟进行一次完整的垃圾收集。这反过来又迫使推广,而不是让它在年轻一代中被收集起来。

如果不是24小时,您应该将其更改为至少1小时,以便GC进行正确的收集。

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

每个JVM选项的列表

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

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,从而产生更大的幸存者空间(比例越小,空间越大)。幸存者比例是伊甸园空间与一个幸存者空间相比的大小。更大的幸存者空间使短命物体在年轻一代中有更长的死亡时间。

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

  • -XX:MaxTenuringThreshold=31–防止年轻一代过早晋升为老年一代。让寿命短的物体在年轻一代中有更长的死亡时间(因此,避免晋升)。此设置的结果是,由于要复制的其他对象,次要GC时间可能会增加。此值和幸存空间大小可能需要调整,以平衡在幸存空间与将长期存在的终身对象之间复制的开销。CMS的默认设置为SurvivorRatio=1024和MaxTenuringThreshold=0,这会导致清除的所有幸存者都被提升。这可能会给单个并发线程带来很大的压力,这些线程会收集终身使用的生成。注意:当与-XX:+UseBiasedLocking一起使用时,此设置应为15。

  • -XX:NewSize=768m–允许指定初始年轻一代尺寸

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

下面是一个更广泛的JVM选项列表。

这是G1的预期行为吗?

我并不觉得奇怪。基本假设是,放入permgen的东西几乎永远不会变成垃圾。所以你会认为permgen GC将是"最后的手段";即JVM只有在其被强制进入完整GC时才会做的事情。(好吧,这个论点远不能证明……但它与以下内容一致。)

我已经看到很多证据表明其他收藏者也有同样的行为;例如

  • permgen垃圾收集需要多次完整GC
  • java GC是怎么回事?PermGen空间已满

我在网上发现了另一篇帖子,有人质疑类似的事情,说G1应该在Perm Gen上执行增量收集,但没有答案。。。

我想我找到了同样的帖子。但有人认为它应该是可能的,这并没有真正的指导意义。

在我们的启动参数中,有什么我可以改进/纠正的吗?

我对此表示怀疑。我的理解是,这是永久GC战略所固有的。

我建议你要么先找到并解决使用了这么多permgen的问题。。。或者切换到不再有permgen堆的Java8:请参阅JDK8中的permgen消除

虽然permgen泄漏是一种可能的解释,但还有其他解释;例如

  • CCD_ 10的过度使用
  • 正在进行大量动态类生成的应用程序代码;例如使用CCD_ 11
  • 巨大的代码库。。。尽管这不会像你所观察到的那样导致permgen流失

在随机尝试JVM选项之前,我会首先尝试找出PermGen变大的根本原因。

  • 您可以启用类加载日志记录(-verbose:class,-XX:+TraceClassLoading-XX:+TTraceClassUnloading,…)并检查输出
  • 在您的测试环境中,您可以尝试(通过JMX)监视类何时加载(java.lang:type=ClassLoadingLoadedClassCount)。这可能有助于您找出应用程序的哪个部分负责
  • 你也可以尝试使用JVM工具列出所有的类(很抱歉,我仍然主要使用jrockit,在那里你可以使用jrcmd。希望Oracle已经将这些有用的功能迁移到Hotspot…)

总之,找出是什么生成了这么多类,然后思考如何减少/调整gc。

干杯,Dimo

我同意上面的答案,因为你真的应该试着找到真正填充你的permgen的东西,我非常怀疑这是因为你想找到根本原因的类加载器泄漏。

JBoss论坛上有一个帖子,介绍了几个这样的诊断案例以及它们是如何修复的。这个答案和本文也讨论了这个问题。在那篇文章中,提到了你能做的最简单的测试:

症状

只有在没有重新启动应用程序服务器。JBoss 4.0.x系列遭遇从这样一个类加载器泄漏。因此,我无法重新部署在JVM耗尽之前,我们的应用程序已经运行了两次以上PermGen内存和崩溃。

解决方案

要识别此类泄漏,请取消部署应用程序,然后触发完全堆转储(确保在此之前触发GC)。然后检查您可以在转储中找到任何应用程序对象。如果是,按照他们的引用找到他们的根,你会发现你的类加载器泄漏。在JBoss 4.0的情况下,唯一的解决方案是为每次重新部署重新启动。

如果你认为重新部署可能有关联,我会首先尝试这样做。这篇博客文章是较早的一篇,做了同样的事情,但也讨论了细节。根据帖子,你可能实际上没有重新部署任何东西,但permgen只是自己填满了。在这种情况下,检查类+添加到permgen中的任何其他内容可能就是方法(正如前面的答案中已经提到的)。

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

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

最新更新