【问题标题】:What causes the JVM to do a major garbage collection?是什么导致 JVM 进行主要的垃圾收集?
【发布时间】:2014-03-07 12:16:40
【问题描述】:

我有一个 Java 应用程序,它在不同的环境中显示不同的 GC 行为。在一个环境中,堆使用图是一个缓慢的锯齿形,每 10 小时左右就会有一次主要的 GC,只有当堆 > 90% 已满时。在另一种环境中,JVM 每小时执行一次主要 GC(此时堆通常在 10% 到 30% 之间)。

我的问题是,是什么因素导致 JVM 决定做一次major GC?

显然它在堆快满时收集,但我猜还有其他一些原因与我的应用程序中的每小时计划任务有关(尽管此时内存使用量没有峰值)。

我假设 GC 行为很大程度上取决于 JVM;我正在使用:

  • Java HotSpot(TM) 64 位服务器 VM 1.7.0_21 Oracle Corporation
  • 没有特定的 GC 选项,因此使用 64 位服务器的默认设置(PS MarkSweep 和 PS Scavenge)

其他信息:

  • 这是在 Tomcat 6 中运行的 Web 应用程序。
  • Perm gen 在两种环境中都徘徊在 10% 左右。
  • 具有锯齿行为的环境具有 7Gb 最大堆,另一个具有 14Gb。

请不要猜测。 JVM 必须有决定何时执行主要 GC 的规则,并且这些规则必须在源代码的某个地方进行编码。如果有人知道它们是什么或记录在哪里,请分享!

【问题讨论】:

  • “在一个环境中……在另一个环境中……” 环境是什么?他们有什么不同?
  • AFAIK 可以通过提示强制 GC 执行垃圾收集,我说强制是因为我认为您不能隐式强制 GC 执行垃圾收集。
  • @CeilingGecko 你是对的,根据规范你不能强制 GC。但是,您可以很好地提出要求,也许 JVM 会遵守。通常,JVM 会在尝试实例化堆中的对象并且没有空间这样做时执行完整的 GC。至少,这是我的理解。完整 GC 还有其他触发器,谷歌搜索“什么触发完整 GC”可以找到大量信息。我相信 JVM 确定它没有空间实例化对象的具体细节由 JVM 开发人员决定。
  • 嗯,不确定,但完整的旧 gen 堆不会触发完整的 gc 吗?如果您正在监视整个堆,它可能看起来大部分是空的,但老一代可能已满?
  • @chris 你有 -Xmx -r -Xms 设置吗?

标签: java garbage-collection jvm jvm-hotspot


【解决方案1】:

我发现了四种可能导致重大 GC 的情况(根据我的 JVM 配置):

  1. old gen 区域已满(即使可以增长,但仍会先运行一次major GC)
  2. perm gen 区域已满(即使可以增长,但仍会先运行主要 GC)
  3. 有人手动调用System.gc():错误的库或与 RMI 相关的东西(请参阅链接 123
  4. 年轻一代区域已满,没有准备好移入老一代(见1

正如其他人评论的那样,可以通过分配大量堆和 permgen 来改进案例 1 和 2,并将 -Xms-Xmx 设置为相同的值(以及 perm 等效项)以避免动态堆调整大小。

使用-XX:+DisableExplicitGC 标志可以避免第三种情况。

案例 4 需要更多涉及的调优,例如 -XX:NewRatio=N(请参阅 Oracle's tuning guide)。

【讨论】:

    【解决方案2】:

    垃圾收集是pretty complicated topic,虽然您可以了解有关此的所有详细信息,但我认为您的案例中发生的事情非常简单。

    Sun 的 Garbage Collection Tuning guide 在“显式垃圾收集”标题下发出警告:

    应用程序可以与垃圾收集交互……通过显式调用完整的垃圾收集……这可以强制在可能不需要时完成主要收集……显式垃圾收集最常见的用法之一发生在 RMI … RMI 会定期强制进行完整收集

    该指南说垃圾回收之间的默认时间是一分钟,但是sun.rmi.dgc.server.gcInterval 下的sun.rmi Properties reference 说:

    默认值为 3600000 毫秒(一小时)。

    如果您在一个应用程序中每小时都看到主要收集,而在另一个应用程序中却没有,这可能是因为该应用程序正在使用 RMI,可能只是在内部使用,并且您没有将 -XX:+DisableExplicitGC 添加到启动标志中。

    禁用显式 GC,或通过设置 -Dsun.rmi.dgc.server.gcInterval=7200000 并观察 GC 是否每两小时发生一次来测试此假设。

    【讨论】:

    • 教程在这一点上是不正确的。用 Java 编写的任何内容都不能“定期强制进行完整收集”。 RMI 调用 System.gc()。这不是一回事。 System.gc() 只是对 GC 的提示。
    • +1 我同意这是最可能的原因。我下周去看看。
    【解决方案3】:

    这取决于您的配置,因为 HotSpot 在不同的 Java 环境中对其自身进行不同的配置。例如,在具有超过 2GB 和两个处理器的服务器中,一些 JVM 将配置为“-server”模式而不是默认的“-client”模式,后者以不同方式配置内存空间(代)的大小,并且具有影响何时进行垃圾收集。

    完全 GC 可以自动发生,但如果您在代码中调用垃圾收集器(例如:使用 System.gc())也可以。自动地,这取决于次要集合的行为方式。

    至少使用了两种算法。如果您使用默认值,则对次要集合使用复制算法,对主要集合使用标记扫描算法。

    复制算法包括将已使用的内存从一个块复制到另一个块,然后清除包含块的空间而不引用它们。 JVM 中的复制算法使用较大的区域来存储第一次创建的对象(称为Eden)和两个较小的区域(称为survivors)。幸存的对象在每个次要收集期间从Eden 复制一次,从survivor 空间复制几次,直到它们成为永久对象并复制到另一个空间(称为tenured 空间),在那里它们只能在主要收集中删除。

    Eden 中的大多数对象都很快死亡,因此第一个集合将幸存的对象复制到幸存者空间(默认情况下要小得多)。有两个幸存者s1s2。每次Eden 填充时,来自Edens1 的幸存对象被复制到s2Edens1 被清除。下一次,来自Edens2 的幸存者被复制回s1。它们不断地从s1 复制到s2s1,直到达到一定数量的副本,或者因为块太大且不适合,或者某些其他标准。然后将幸存的内存块复制到tenured 代。

    tenured 对象不受次要集合的影响。它们累积直到该区域变满(或调用垃圾收集器)。然后 JVM 将在主要集合中运行标记扫描算法,该算法将仅保留仍然具有引用的幸存对象。

    如果您有较大的对象无法放入幸存者中,它们可能会被直接复制到tenured 空间,这将更快地填充并且您将更频繁地获得主要收藏品。

    另外,幸存者空间的大小,s1s2 之间的副本数量,Eden 的大小与 s1s2 的大小相关,终身代的大小,所有这些都可能使用 JVM 人体工程学 在不同的环境中自动进行不同的配置,这可能会自动选择 -server-client 行为。您可以尝试以 -server-client 的身份运行这两个 JVM,并检查它们的行为是否仍然不同。

    【讨论】:

    • 对主要/次要 GC 算法的一般解释很好,但这不是我的问题。
    【解决方案4】:

    即使这会导致投票失败...我最好的猜测(您必须对此进行测试)是堆需要扩展,并且当这种情况发生时将触发完整的 gc。并非所有内存都立即分配给 JVM。

    您可以通过将 -Xms 和 -Xmx 设置为相同的值来测试这一点,例如每个 7GB

    【讨论】:

    • FWIW,OP 已经评论说他们确实在这样做。在一种环境中,他/她正在设置-Xms14g -Xmx14g,而在另一种环境中,他/她正在设置-Xms7g -Xmx7g
    猜你喜欢
    • 2017-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 2023-03-07
    • 2012-06-15
    • 2010-11-10
    相关资源
    最近更新 更多