返回首页 JVM 实用参数系列

新生代垃圾回收

原文链接 作者: PATRICK PESCHLOW;译者:严亮

本部分,我们将关注堆 (heap) 中一个主要区域,新生代 (young generation)。首先我们会讨论为什么调整新生代的参数会对应用的性能如此重要,接着我们将学习新生代相关的 JVM 参数。

单纯从 JVM 的功能考虑,并不需要新生代,完全可以针对整个堆进行操作。新生代存在的唯一理由是优化垃圾回收 (GC) 的性能。更具体说,把堆划分为新生代和老年代有 2 个好处:简化了新对象的分配 (只在新生代分配内存), 可以更有效的清除不再需要的对象 (即死对象)(新生代和老年代使用不同的 GC 算法)

通过广泛研究面向对象实现的应用,发现一个共同特点:很多对象的生存时间都很短。同时研究发现,新生对象很少引用生存时间长的对象。结合这 2 个特点,很明显 GC 会频繁访问新生对象,例如在堆中一个单独的区域,称之为新生代。在新生代中,GC 可以快速标记回收” 死对象”,而不需要扫描整个 Heap 中的存活一段时间的” 老对象”。

SUN/Oracle 的 HotSpot JVM 又把新生代进一步划分为 3 个区域:一个相对大点的区域,称为“伊甸园区 (Eden)”;两个相对小点的区域称为“From 幸存区 (survivor)” 和“To 幸存区 (survivor)”。按照规定,新对象会首先分配在 Eden 中 (如果新对象过大,会直接分配在老年代中)。在 GC 中,Eden 中的对象会被移动到 survivor 中,直至对象满足一定的年纪 (定义为熬过 GC 的次数),会被移动到老年代。

基于大多数新生对象都会在 GC 中被收回的假设。新生代的 GC 使用复制算法。在 GC 前 To 幸存区 (survivor) 保持清空,对象保存在 Eden 和 From 幸存区 (survivor) 中,GC 运行时,Eden 中的幸存对象被复制到 To 幸存区 (survivor)。针对 From 幸存区 (survivor) 中的幸存对象,会考虑对象年龄,如果年龄没达到阀值 (tenuring threshold),对象会被复制到 To 幸存区 (survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和 From 幸存区中只保存死对象,可以视为清空。如果在复制过程中 To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To 幸存区会调换下名字,在下次 GC 时,To 幸存区会成为 From 幸存区。

https://blog.codecentric.de/files/2011/08/young_gc.png

上图演示 GC 过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象

总结一下,对象一般出生在 Eden 区,年轻代 GC 过程中,对象在 2 个幸存区之间移动,如果对象存活到适当的年龄,会被移动到老年代。当对象在老年代死亡时,就需要更高级别的 GC,更重量级的 GC 算法 (复制算法不适用于老年代,因为没有多余的空间用于复制)

现在应该能理解为什么新生代大小非常重要了 (译者,有另外一种说法:新生代大小并不重要,影响 GC 的因素主要是幸存对象的数量),如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优。这就需要 JVM 参数了

-XX:NewSize and -XX:MaxNewSize

就像可以通过参数 (-Xms and -Xmx) 指定堆大小一样,可以通过参数指定新生代大小。设置 XX:MaxNewSize 参数时,应该考虑到新生代只是整个堆的一部分,新生代设置的越大,老年代区域就会减少。一般不允许新生代比老年代还大,因为要考虑 GC 时最坏情况,所有对象都晋升到老年代。(译者: 会发生 OOM 错误) -XX:MaxNewSize 最大可以设置为 - Xmx/2。

考虑性能,一般会通过参数 -XX:NewSize 设置新生代初始大小。如果知道新生代初始分配的对象大小 (经过监控),这样设置会有帮助,可以节省新生代自动扩展的消耗。

-XX:NewRatio

可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=3 指定老年代 / 新生代为 3/1。 老年代占堆大小的 3/4,新生代占 1/4 。

如果针对新生代,同时定义绝对值和相对值,绝对值将起作用。下面例子:

$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp

以上设置,JVM 会尝试为新生代分配四分之一的堆大小,但不会小于 32MB 或大于 521MB

在设置新生代大小问题上,使用绝对值还是相对值,不存在通用准则 。如果了解应用的内存使用情况, 设置固定大小的堆和新生代更有利,当然也可以设置相对值。如果对应用的内存使用一无所知,正确的做法是不要设置任何参数,如果应用运行良好。很好,我们不用做任何额外动作。如果遇到性能或 OutOfMemoryErrors,在调优之前,首先需要进行一系列有目的的监控测试,缩小问题的根源。

-XX:SurvivorRatio

参数 -XX:SurvivorRatio 与 -XX:NewRatio 类似,作用于新生代内部区域。-XX:SurvivorRatio 指定伊甸园区 (Eden) 与幸存区大小比例。 例如, -XX:SurvivorRatio=10 表示伊甸园区 (Eden) 是 幸存区 To 大小的 10 倍 (也是幸存区 From 的 10 倍)。 所以, 伊甸园区 (Eden) 占新生代大小的 10/12, 幸存区 From 和幸存区 To 每个占新生代的 1/12 。 注意, 两个幸存区永远是一样大的。

设定幸存区大小有什么作用? 假设幸存区相对伊甸园区 (Eden) 太小, 相应新生对象的伊甸园区 (Eden) 永远很大空间, 我们当然希望, 如果这些对象在 GC 时全部被回收, 伊甸园区 (Eden) 被清空, 一切正常。 然而, 如果有一部分对象在 GC 中幸存下来, 幸存区只有很少空间容纳这些对象。 结果大部分幸存对象在一次 GC 后,就会被转移到老年代 , 这并不是我们希望的。 考虑相反情况, 假设幸存区相对伊甸园区 (Eden) 太大, 当然有足够的空间,容纳 GC 后的幸存对象。 但是过小的伊甸园区 (Eden), 意味着空间将越快耗尽,增加新生代 GC 次数,这是不可接受的。

总之, 我们希望最小化短命对象晋升到老年代的数量,同时也希望最小化新生代 GC 的次数和持续时间。 我们需要找到针对当前应用的折中方案, 寻找适合方案的起点是 了解当前应用中对象的年龄分布情况。

-XX:+PrintTenuringDistribution

参数 -XX:+PrintTenuringDistribution 指定 JVM 在每次新生代 GC 时,输出幸存区中对象的年龄分布。例如:

Desired survivor size 75497472 bytes, new threshold 15 (max 15)

  • age 1: 19321624 bytes, 19321624 total
  • age 2: 79376 bytes, 19401000 total
  • age 3: 2904256 bytes, 22305256 total

第一行说明幸存区 To 大小为 75 MB。 也有关于老年代阀值 (tenuring threshold) 的信息, 老年代阀值,意思是对象从新生代移动到老年代之前,经过几次 GC(即, 对象晋升前的最大年龄)。 上例中, 老年代阀值为 15, 最大也是 15。

之后行表示,对于小于老年代阀值的每一个对象年龄,本年龄中对象所占字节 (如果当前年龄没有对象, 这一行会忽略)。 上例中, 一次 GC 后幸存对象大约 19 MB, 两次 GC 后幸存对象大约 79 KB,三次 GC 后幸存对象大约 3 MB 。 每行结尾,显示直到本年龄全部对象大小。 所以, 最后一行的 total 表示幸存区 To 总共被占用 22 MB 。 幸存区 To 总大小为 75 MB , 当前老年代阀值为 15,可以断定在本次 GC 中,没有对象会移动到老年代。现在假设下一次 GC 输出为:

Desired survivor size 75497472 bytes, new threshold 2 (max 15)

  • age 1: 68407384 bytes, 68407384 total
  • age 2: 12494576 bytes, 80901960 total
  • age 3: 79376 bytes, 80981336 total
  • age 4: 2904256 bytes, 83885592 total

对比前一次老年代分布。明显的, 年龄 2 和年龄 3 的对象还保持在幸存区中,因为我们看到年龄 3 和 4 的对象大小与前一次年龄 2 和 3 的相同。同时发现幸存区中, 有一部分对象已经被回收, 因为本次年龄 2 的对象大小为 12MB ,而前一次年龄 1 的对象大小为 19 MB。最后可以看到最近的 GC 中,有 68 MB 新对象,从伊甸园区移动到幸存区。

注意, 本次 GC 幸存区占用总大小 84 MB - 大于 75 MB。 结果, JVM 把老年代阀值从 15 降低到 2,在下次 GC 时,一部分对象会强制离开幸存区,这些对象可能会被回收 (如果他们刚好死亡) 或移动到老年代。

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold and -XX:TargetSurvivorRatio

参数 -XX:+PrintTenuringDistribution 输出中的部分值可以通过其它参数控制。通过 -XX:InitialTenuringThreshold 和 -XX:MaxTenuringThreshold 可以设定老年代阀值的初始值和最大值。另外, 可以通过参数 -XX:TargetSurvivorRatio 设定幸存区的目标使用率。 例如, -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 设定老年代阀值的上限为 10, 幸存区空间目标使用率为 90%。

有多种方式, 设置新生代行为,没有通用准则。我们必须清楚以下 2 中情况:

  1. 如果从年龄分布中发现,有很多对象的年龄持续增长,在到达老年代阀值之前。这表示 -XX:MaxTenuringThreshold 设置过大
  2. 如果 -XX:MaxTenuringThreshold 的值大于 1,但是很多对象年龄从未大于 1。应该看下幸存区的目标使用率。如果幸存区使用率从未到达,这表示对象都被 GC 回收,这正是我们想要的。 如果幸存区使用率经常达到,有些年龄超过 1 的对象被移动到老年代中。这种情况,可以尝试调整幸存区大小或目标使用率。

-XX:+NeverTenure and -XX:+AlwaysTenure

最后,我们介绍 2 个颇为少见的参数,对应 2 种极端的新生代 GC 情况。设置参数 -XX:+NeverTenure,对象永远不会晋升到老年代。当我们确定不需要老年代时,可以这样设置。这样设置风险很大, 并且会浪费至少一半的堆内存。相反设置参数 -XX:+AlwaysTenure,表示没有幸存区,所有对象在第一次 GC 时,会晋升到老年代。

没有合理的场景使用这个参数。可以在测试环境中,看下这样设置会发生什么有趣的事。但是并不推荐使用这些参数。

结论 适当的配置新生代非常重要,有相当多的参数可以设置新生代。然而,单独调整新生代,而不考虑老年代是不可能优化成功的。当调整堆和 GC 设置时,我们总是应该同时考虑新生代和老年代。

在本系列的下面 2 部分,我们将讨论 HotSpot JVM 中老年代 GC 策略,我们会学习 “吞吐量 GC 收集器” 和 “并发低延迟 GC 收集器”,也会了解收集器的基本准则,算法和调整参数。

上一篇: 内存调优 下一篇: 吞吐量收集器