G1 垃圾收集器:烫发生成无限期填满,直到执行完整的 GC

2022-09-01 12:06:50

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

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

几个月来,我们一直在使用512 MB的最大烫发大小,在我们的分析过程中,使用ParallelGC,烫发大小通常会停止在390 MB左右增长。然而,在我们切换到G1之后,上述行为开始发生。我尝试将最大烫发大小增加到1 GB甚至1,5 GB,但仍然会发生完整的GC(它们只是不那么频繁)。

在此链接中,您可以看到我们正在使用的分析工具(YourKit Java Profiler)的一些屏幕截图。请注意,当触发完整GC时,伊甸园和旧世代有很多可用空间,但烫发大小处于最大值。在完整GC之后,Perm大小和加载类的数量急剧减少,但它们再次开始上升并且循环重复。代码缓存很好,永远不会超过38 MB(在本例中为35 MB)。

下面是 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.0M)] [倍数: user=10.16 sys=0.59, real=23.64 秒]

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

以下是一些环境信息:

java 版本 “1.7.0_45”

Java(TM) SE Runtime Environment (build 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上执行增量收集,但没有答案......

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


答案 1

烫发基因生长的原因

  • 很多类,尤其是JSP。
  • 很多静态变量。
  • 存在类装入器泄漏。

对于那些不知道的人,这里有一个简单的方法来思考PremGen是如何填满的。年轻一代没有足够的时间让东西过期,所以他们被转移到了老一代的空间。当年轻或年老一代中的对象被收集并且该类不再被引用时,它就会从彼尔姆将军那里“卸载”,如果年轻和年老的一代没有得到GC'd,那么彼尔姆将军也不会,一旦填满,它需要一个完整的世界GC。有关详细信息,请参阅展示永久一代


切换到内容管理系统

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


隐藏的陷阱

如果您使用的是 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正在使用什么,那么你需要将以下内容添加到你的.您将获得每个JVM选项及其设置内容的列表。注意:它必须位于要查看的 JVM 中才能使用它。如果你在外部运行它,你将无法看到JBoss正在运行的JVM中发生了什么。standalone.xml

set "JAVA_OPTS= -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal %JAVA_OPTS%"

当我们只对修改后的标志感兴趣时,可以使用快捷方式。

-XX:+PrintcommandLineFlags

诊断

使用 jmap 确定哪些类正在消耗永久生成空间。输出将显示

  • 类装入器
  • 类数
  • 字节
  • 父加载程序
  • 活着/死了
  • 类型
  • 总数

    jmap -permstat JBOSS_PID  >& permstat.out
    

JVM 选项

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

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

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

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

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

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

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


答案 2

这是G1的预期行为吗?

我不觉得这令人惊讶。基本假设是,放入permgen的东西几乎永远不会变成垃圾。因此,您会期望烫发GC将是“最后的手段”;也就是说,只有当JVM被迫进入一个完整的GC时,它才会做一些事情。(好吧,这个论点远非证明...但它与以下内容一致。

我看到很多证据表明其他收藏家也有同样的行为。例如:

我在网上发现了另一个帖子,有人质疑非常相似的东西,并说G1应该在Perm Gen上执行增量收集,但没有答案......

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

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

我怀疑。我的理解是,这是烫金GC策略所固有的。

我建议您首先跟踪并修复使用如此多permgen的内容......或者切换到不再有 permgen 堆的 Java 8:请参阅 JDK 8 中的 PermGen 消除

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

  • 过度使用 ,String.intern()
  • 正在执行大量动态类生成的应用程序代码;例如,使用 ,DynamicProxy
  • 一个巨大的代码库...虽然这不会导致permgen流失,因为你似乎正在观察。