【问题标题】:G1 garbage collector: Perm Gen fills up indefinitely until a Full GC is performedG1 垃圾收集器:Perm Gen 无限期填满,直到执行 Full GC
【发布时间】:2013-11-28 20:31:43
【问题描述】:

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

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

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

this link 中,您可以看到我们正在使用的分析工具(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: [完整 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 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 的预期行为吗?我在网上找到另一个帖子,有人质疑非常相似的事情,并说 G1 应该在 Perm Gen 上执行增量收集,但没有答案......

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

【问题讨论】:

  • 这是其他人在非常相似的问题上寻求帮助的链接:mail.openjdk.java.net/pipermail/hotspot-gc-use/2013-October/…
  • 我会尝试添加-verbose:gc 以查看更多详细信息,我也可以考虑尝试Chronon DVR
  • 我将添加该选项并让服务器运行一段时间以查看是否获得更多信息,但我很清楚导致完整 GC 运行的原因,我只是不'不明白这是否是 G1 的正确行为...
  • 非常有趣的博文:mechanical-sympathy.blogspot.nl/2013/07/…,总而言之太长,但最后一段总结得很好:“如果延迟峰值是由 GC 引起的,那么投资调整 CMS 或 G1 以查看您的延迟目标是否达到目标可以满足。有时这可能是不可能的,因为高分配和提升率以及低延迟要求。GC 调整可以成为一项高技能的练习,通常需要更改应用程序以降低对象分配率或对象生命周期。"
  • Joshua Wilson 的帖子捕捉了 G1GC 与 CMS 的一些优点,但关于为什么会发生这种情况的问题的答案可能是在较早的电子邮件对话中,有趣的部分实际上从这里开始:mail.openjdk.java.net/pipermail/hotspot-gc-use/2010-July/…。他们讨论了某些区域可能永远不会被收集的可能性,因此会强制进行完整的 GC。在整个讨论过程中有一些非常有趣的提示,但不幸的是我在那里找不到明确的答案。也许您会根据自己的代码经验找到一些建议。

标签: java garbage-collection jboss7.x g1gc


【解决方案1】:

Perm Gen 增长的原因

  • 很多类,尤其是 JSP。
  • 大量静态变量。
  • 存在类加载器泄漏。

对于那些不知道的人,这里有一个简单的方法来思考 PremGen 如何填充。年轻一代没有足够的时间让事情过期,所以他们被转移到了老一代的空间。 Perm Gen 拥有 Young Gen 和 Old Gen 中对象的类。当 Young Gen 或 Old Gen 中的对象被收集并且不再引用该类时,它将从 Perm Gen 中“卸载”。如果 Young 和 Old Gen Old Gen 没有得到 GC,Perm Gen 也没有,一旦填满它就需要 Full stop-the-world GC。欲了解更多信息,请参阅Presenting the Permanent Generation


切换到 CMS

我知道您正在使用 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 正在使用什么,则需要将以下内容添加到您的 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 Options

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

  • -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 options 列表。

【讨论】:

  • 我们确实使用了 RMI GC 间隔设置,我什至尝试将其删除,但没有任何区别。当 Perm Gen 填满时,FullGC 显然会被触发。话虽如此,我不确定更改 Eden 大小或任期阈值是否会有所帮助,在 GC 日志中我根本没有看到提升失败(或“到空间溢出”),老一代甚至没有半满Full GC 被触发。我将继续尝试不同的参数,但我认为 G1 的这种行为非常奇怪。可能是我们的应用有问题,但是ParallelGC没有这个问题(但是有其他问题)
  • 我们确实尝试过 G1,但后来又回到 UseConcMarkSweepGC 并进行了这些更改。要清楚这些不是随机的 JVM 设置,这些是我们使用的。底部的注释旨在明确您使用 1 选项或其他选项,而不是两者,因为它们做同样的事情。此外,我们遇到的问题不是促销失败,而是促销经常发生的事实。那是把事情推到 Perm 领域,而他们本不应该到达那里。
  • 我更新了帖子,如果这有帮助或者您有更多问题,请告诉我。抄送@ElliottFrisch
  • 您提供的其他信息会有所帮助。在这里和其他地方大量阅读之后,我们似乎真的从错误的角度看待问题:G1 不是问题的原因,它只是帮助暴露了它。我们将首先解决加载这么多类的问题(原因之一是我们的应用程序对远程 EJB 有很多调用,而这些调用根本不需要远程),同时试验不同的收集器和不同的参数,看看什么效果最好。
  • 我会尝试您建议的配置,因为我们使用的分析工具(非常好,顺便说一句)显示了您提到的内容:促销活动过于频繁,有时过早,这使得旧Gen 和 Perm Gen 填充得更快,从而产生更完整的 GC。
【解决方案2】:

这是 G1 的预期行为吗?

我觉得这并不奇怪。基本假设是放入 permgen 的东西几乎永远不会变成垃圾。所以你会期望 permgen GC 将是“最后的手段”;即 JVM 仅在强制进入完整 GC 时才会执行的操作。 (好吧,这个论点远非证明......但它与以下内容一致。)

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

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

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

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

我对此表示怀疑。我的理解是,这是 permgen GC 策略所固有的。

我建议您要么首先追踪并修复正在使用这么多 permgen 的东西……要么切换到不再有 permgen 堆的 Java 8:请参阅PermGen elimination in JDK 8

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

  • 过度使用String.intern()
  • 执行大量动态类生成的应用程序代码;例如使用DynamicProxy
  • 一个巨大的代码库...尽管这不会像您观察到的那样导致 permgen churn

【讨论】:

  • 感谢您的信息。这确实是我们现在正在考虑的方式:找出导致这么多类被加载的原因,然后调整我们的 GC 配置。正如我在另一条评论中所说,G1 绝对不是问题的原因,但它帮助暴露了它。
【解决方案3】:

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

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

总之,找出产生这么多类的原因,然后思考如何减少/调整 gc。

干杯, 迪莫

【讨论】:

  • 我认为你是对的,也许这是我们目前最好的选择。我开始认为这可能在 ParallelGC 之前没有发生过,因为主要收集更频繁,这阻止了 Perm Gen 增长过多。
  • 我们从错误的角度看待问题,因为 G1 不是原因,它只是帮助暴露了它。我们正在调查这个问题,并且已经发现其中一个主要原因是我们的应用程序有很多远程 EJB 被调用,所以我们首先解决这个问题。同时,我们只需要尝试不同的 GC 配置,直到找到最适合我们的配置。
【解决方案4】:

我同意the answer above 的观点,因为您应该真正尝试找出实际填充您的 permgen 的内容,我严重怀疑这是您想要找到根本原因的一些类加载器泄漏。

JBoss 论坛中有this thread 经历了几个这样的诊断案例以及它们是如何修复的。 this answerthis article 也讨论了这个问题。在那篇文章中提到了可能是您可以做的最简单的测试:

症状

仅当您重新部署应用程序时才会发生这种情况 重新启动应用程序服务器。 JBoss 4.0.x 系列遭受重创 从这样的类加载器泄漏。结果我无法重新部署 我们的应用程序在 JVM 用完之前两次以上 PermGen 内存和崩溃。

解决方案

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

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

如果这不能提供更多见解,我下一步将尝试plumbr tool。他们也有一种guarantee on finding the leak for you

【讨论】:

  • 我们通常不会在不重新启动 JBoss 的情况下重新部署我们的应用程序,我现在真的认为 Perm Gen 这么快被填满的事实是因为我们的应用程序是如何实现的,G1 实际上只是帮助向我们揭露这个问题。
【解决方案5】:

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

【讨论】:

  • 如果你使用 -Xloggc 和 -XX:+PrintGCDetails(它是一个遗留的同义词),你不需要 -verbose:gc。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-24
  • 1970-01-01
  • 2014-02-19
  • 2012-11-26
  • 2014-09-02
  • 2013-06-02
  • 1970-01-01
相关资源
最近更新 更多