JVM 什么情况下会触发主要垃圾回收?

8
我有一个Java应用程序,它在不同的环境中显示不同的GC行为。在一个环境中,堆使用图是一个缓慢的锯齿状,每隔10个小时或更长时间进行一次major GC,只有当堆占用率> 90%时才进行。在另一个环境中,JVM每小时准点进行major GC(此时堆通常在10%到30%之间)。
我的问题是,导致JVM决定执行major GC的因素是什么?
显然,当堆接近满时会进行收集,但还有其他原因在起作用,我猜测这与我的应用程序内部的每小时计划任务有关(尽管此时没有内存使用量的飙升)。
我认为GC行为严重依赖于JVM;我正在使用:
- Java HotSpot(TM)64位Server VM 1.7.0_21 Oracle Corporation - 没有特定的GC选项,因此使用64位服务器的默认设置(PS MarkSweep和PS Scavenge)
其他信息:
- 这是在Tomcat 6中运行的Web应用程序。 - Perm gen在两个环境中都约为10%。 - 具有锯齿行为的环境具有7Gb最大堆,而另一个环境具有14Gb。
请不要猜测。JVM必须有规则来决定何时执行major GC,这些规则必须在源代码深处编码。如果有人知道它们在哪里记录或文档化,请分享!

在一个环境中... 在另一个环境中... 这些环境是什么?它们有何不同之处? - T.J. Crowder
据我所知,GC可以通过提示被强制执行垃圾回收,我说强制是因为我认为你不能隐式地强制GC执行垃圾回收。 - Ceiling Gecko
@CeilingGecko 你是正确的,根据规范你无法强制进行垃圾回收。但是,你可以礼貌地请求,也许JVM会答应。通常情况下,当JVM尝试在堆中实例化对象并且没有足够的空间时,它会执行完整的GC。至少,这是我的理解。有其他触发器可以导致进行完整的GC,而使用Google搜索“什么会触发完整的GC”可以找到大量信息。我认为,JVM在确定没有足够空间来实例化对象的具体情况方面留给了JVM开发人员。 - CodeChimp
在我曾经工作的另一个JVM上,GC周期是由堆分配的数量触发的--在分配了这么多MB的堆之后,会触发GC。我想这里可能也有类似的机制在起作用。 - Hot Licks
@chris:我认为我的表述相当清晰。这是标准的调试方法:如果A做了一件事,而B做了另一件事,请查看A和B之间的区别。例如,与内存相关的命令行参数是不同的。也许服务器具有不同数量的内存。或者是不同版本的Linux(即使只是内核的小版本)。或者有不同的其他进程在运行。或... - T.J. Crowder
显示剩余3条评论
4个回答

7
我已经找到了四种情况可以引起主要GC(根据我的JVM配置):
  1. 老年代区域已满(即使它可以扩大,主要GC仍将首先运行)
  2. 永久代区域已满(即使它可以扩大,主要GC仍将首先运行)
  3. 有人手动调用 System.gc():糟糕的库或与RMI相关的内容(请参见链接 1, 23
  4. 所有年轻代区域都已满,并且没有可以移入老年代的内容(请参见链接1
如其他评论所述,情况1和2可以通过分配足够的堆和permgen,并将-Xms-Xmx 设置为相同的值(与permgen等效值一起),以避免动态调整堆大小。
使用-XX: + DisableExplicitGC 标志可以避免情况3。
情况4需要更深入的调整,例如 -XX:NewRatio = N(请参见Oracle的调整指南)。

6
垃圾回收是一个相当复杂的话题,虽然你可以学习所有有关它的细节,但我认为在你的情况下正在发生的事情非常简单。
Sun的《垃圾回收调整指南》在“显式垃圾回收”标题下警告说:
应用程序可以通过显式调用完全垃圾回收与垃圾回收进行交互。这可能会在不必要时强制执行主要收集。 显式垃圾回收最常见的用途之一是在RMI中使用。 RMI定期强制进行完全收集。
该指南称默认垃圾回收时间间隔为1分钟,但sun.rmi属性参考文献在sun.rmi.dgc.server.gcInterval下说:
默认值为3600000毫秒(一小时)。
如果你在一个应用程序中每小时看到大量的收集,而在另一个应用程序中没有,那么这可能是因为该应用程序正在使用RMI,可能仅在内部使用,并且你还没有将-XX:+DisableExplicitGC添加到启动标志中。
禁用显式GC,或通过设置-Dsun.rmi.dgc.server.gcInterval=7200000并观察是否每两个小时发生一次GC来测试此假设。

这个教程在这一点上是错误的。Java中没有任何东西可以“强制定期进行完整收集”。RMI调用System.gc()。这不是同一件事。System.gc()只是对GC的提示。 - user207421
+1 我同意这很可能是原因。我下周会去检查一下。 - chris

4
这取决于您的配置,因为HotSpot在不同的Java环境中配置不同。例如,在具有2GB以上和两个处理器的服务器上,一些JVM将配置为“-server”模式,而不是默认的“-client”模式,这会以不同的方式配置内存空间(代),并且会影响垃圾回收的发生时间。
完全GC可以自动发生,但也可以在代码中调用垃圾回收器(例如使用System.gc())。自动情况下,它取决于小集合的行为如何。
至少有两种算法正在使用。如果您使用默认值,则在小集合中使用复制算法,在主要集合中使用标记清除算法。
复制算法包括将已使用的内存从一个块复制到另一个块,然后清除包含没有引用的块的空间。 JVM中的复制算法使用用于第一次创建对象的大型区域(称为Eden),以及两个较小的区域(称为survivors)。幸存的对象在每次小集合期间从Edensurvivor空间中复制一次,直到它们成为老年对象并被复制到另一个空间(称为tenured空间),在那里它们只能在主要集合中删除。 Eden中的大多数对象很快就会死亡,因此第一次收集将幸存的对象复制到幸存者空间(默认情况下要小得多)。有两个幸存者s1s2。每次Eden填充时,从Edens1中幸存的对象被复制到s2中,清除Edens1。下一次,来自Edens2的幸存者将被复制回s1。它们继续从s1复制到s2,然后到s1,直到达到一定数量的副本,或者因为块太大而无法适应,或者其他标准。然后将幸存的内存块复制到tenured代。 tenured对象不受小集合的影响。它们积累直到该区域变满(或调用垃圾收集器)。然后JVM将在主要集合中运行标记清除算法,仅保留仍具有引用的幸存对象。
如果您有无法适应幸存者的较大对象,则可能直接将它们复制到tenured空间,这将更快地填充并更频繁地进行主要集合。
此外,幸存者空间的大小、s1s2 之间的副本数量、与 s1s2 大小相关的 Eden 大小、老年代的大小等等在不同的环境下可能会自动配置,这基于 JVM 自适应调整,它可能会自动选择 -server-client 行为。您可以尝试将两个 JVM 都作为 -server-client 运行,并检查它们是否仍然有不同的行为。

很好的对主/次要GC算法的一般解释,但那不是我的问题。 - chris

1
即使这样可能会被投票降低... 我的最佳猜测(你需要测试一下)是堆需要扩展,当这发生时,将触发完整的GC。并非所有内存都一次性分配给JVM。
您可以通过将-Xms和-Xmx设置为相同的值来进行测试,例如每个7GB。

顺便提一下,楼主已经回复说他们确实在这样做。在一个环境中,他/她设置了“-Xms14g -Xmx14g”,而在另一个环境中,设置为“-Xms7g -Xmx7g”。 - T.J. Crowder

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