集册 JVM 原理详解 新生代垃圾回收

新生代垃圾回收

欢马劈雪     最近更新时间:2020-11-09 06:02:02

494

本部分,我们将关注堆 (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

展开阅读全文