JEP 404: Generational Shenandoah (Experimental) | 分代 Shenandoah(实验性)
概述
增强 Shenandoah 垃圾收集器 的实验性分代收集能力,以改进可持续吞吐量、负载峰值恢复力和内存利用率。
目标
主要目标是在不破坏非分代 Shenandoah 的情况下提供一个实验性的分代模式。我们打算在未来版本中将分代模式作为默认选项。
相对于非分代 Shenandoah 的其他目标包括:
- 在不影响低 GC 暂停的情况下减少持续的内存占用。
- 减少 CPU 使用和功耗。
- 降低在分配峰值期间发生退化和完全收集的风险。
- 维持高吞吐量。
- 继续支持压缩对象指针。
- 初始阶段支持 x64 和 AArch64 指令集,并随着此实验模式逐步准备成为默认选项而添加对其他指令集的支持。
非目标
- 不是取代非分代 Shenandoah 的目标,后者将继续作为默认操作模式,并且在其性能或功能上不会出现倒退。
- 不是改善每种可想象工作负载性能的目标。分代系统会根据需要动态适应近似于非分代系统,但对于某些工作负载,从一开始就保持单一世代可能仍是更优的选择。尽管如此,我们预计大多数用例都将受益于分代收集。
- 相较于传统的全停顿 GC(stop-the-world GC),不是旨在改善 CPU 和电力使用的效率。如果可以容忍较长的暂停时间,G1 等其他收集器仍可能提供更节能的行为。
- 不是最大化变更器(mutator)吞吐量的目标。如果可以接受更长的暂停时间,Parallel 收集器等其他收集器在某些平台上可能提供更高的吞吐量。
- 在初始发布时,人体工程学启发式方法可能无法在所有工作负载上提供最优行为。
成功指标
- 使用 SPECjbb2015 、 HyperAlloc 、 Extremem 和 Dacapo 对分代 Shenandoah 与非分代 Shenandoah 进行基准测试。
- 对比 HyperAlloc、Extremem 以及类似工作负载的操作范围(即分配率、堆占用率和暂停时间目标的组合),成功的运行减少了或消除了分配阻塞的数量,并降低了进行完全或退化收集的需求。
动机
具备并发整理能力的垃圾收集器能够将 GC 暂停时间完全融入到其他常见 JVM 暂停的单个数字毫秒范围内,同时几乎不妨碍变更器执行速度。非分代 Shenandoah 垃圾收集已经为延迟敏感型 Java 应用程序提供了这种理想的 GC 行为。然而,它只能在有限的操作范围内(即堆占用率和分配率的组合)实现这一点。
一种经典的最小化平均 GC 成本的方法是采用“大部分对象早亡”的 分代假设,并集中处理年轻因此大多已死的对象。相较于分代收集器 G1、CMS 和 Parallel,非分代 Shenandoah 往往需要更多的堆空间余量,并更加努力地回收被不可达对象占据的空间。
基于区域的分代收集器能够动态调整其各代大小和复制策略以响应对象人口统计的变化,使收集器能够适应不遵循分代假设的工作负载。即使存活对象在年轻一代中被复制的频率超过了必要次数,这一成本也常常被与非分代收集器相比减少长期对象标记频率所带来的收益所掩盖。
一种既能达到低暂停时间又能在其他性能方面保持竞争力的收集器应该既是并发的又是分代的,并能动态调整其年轻代的大小及相关操作参数。
描述
此次对 Shenandoah 垃圾收集器的增强将其 Java 堆分为两个世代。如同其他分代收集器一样,GC 努力主要集中在年轻代,即由变更器进行分配并且临时对象可以用较少的努力回收的一代。我们提出以下方法作为初步实施的方案。
每个世代上运行的收集算法都紧密基于非分代 Shenandoah。在年轻代内部,分代 Shenandoah 使用与传统 Shenandoah 相同的启发式方法来区分持有新分配对象的内存区域和持有在一个或多个最近的年轻代收集中幸存下来的对象的内存区域。
每个世代由 Shenandoah 堆的部分区域组成。在任何给定时间,一个区域要么被认为是空闲的,要么专用于年轻或老年代。每个世代的大小由其所占有的区域加上一部分空闲区域配额决定。超出各自另一代空闲配额的情况是可以容忍的,但这会加速收集触发,并可能导致退化和完全收集。我们正在积极完善控制收集阶段调度、年轻代大小、晋升年龄及其他自动调优机制的算法。
Shenandoah 拥有独特的加载引用屏障(LRB),支持 32 位构建和 64 位构建中的压缩 32 位对象指针(“compressed oops”)。为了限制对变更器的影响,我们在两代中都使用了相同的 LRB,没有任何更改,并使用单个疏散者来处理旧和新的收集工作。典型的疏散阶段要么仅从年轻区域收集垃圾,要么从年轻和老年的组合区域收集垃圾。这种行为模仿了 G1 的年轻和混合收集。相较于 G1 的主要改进在于分代 Shenandoah 的年轻和混合收集是与变更器并发执行的。
针对不同世代的标记阶段很大程度上彼此解耦。当年轻代标记和疏散多次发生时,后台会继续进行并发的老年代标记。老年代标记可以被抢占,以便执行更高优先级的年轻代收集。一旦老年代标记完成,后续的疏散和引用更新将包括老年代区域,直到整个老年代收集集合被处理完毕。
对于记忆集实现,我们使用了从 Parallel 和 CMS GC 实现借来的现有卡片标记代码,并补充了允许记忆集扫描与变更器执行并发运行的新代码。
非分代 Shenandoah 现有的 SATB 屏障已被泛化,以满足年轻代和老年代并发标记的共同需求。对 SATB 缓冲区的后处理区别对待指向老年代内存和年轻代内存的引用,但这些屏障的快速路径保持不变。
使用
新的分代特性是 Shenandoah 代码库的一部分,但如果未通过 JVM 命令行选项激活,则不会有运行时影响
-XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational
在这种情况下,Shenandoah 将使用其分代模式。
项目维基将提供关于如何配置和调整 JVM 以有效运行带有 Shenandoah GC 的应用程序的分代模式的详细信息。
替代方案
Azul Systems 的 C4 收集器已经是分代的,但不开放源代码。ZGC 的分代模式在 JDK 21 中实现。这些选项都不支持压缩对象指针。然而,我们看到的绝大多数 Java 堆(例如,在云服务中)大小都在 32GB 以下,因此能够利用这一节省空间和提高性能的特点。
测试
大多数现有的功能和压力测试都是与收集器无关的,可以按原样重用。我们将为新的分代模式集成额外的测试运行配置,并加入针对新模式的功能、性能和压力测试。
当前性能优化的重点是在 Linux 上的 x86 和 AArch64 架构。SAP 已将分代模式移植到 PowerPC 并在该平台上进行了测试。CI 测试在 Linux、macOS 和 Windows 上运行。对于其他平台的分代模式支持可以随后实施和优化。
风险和假设
记忆集操作,特别是扫描,可能会增加暂停时间。
与记忆集相关的屏障可能会增加变更器的开销。
自动配置世代大小、对象晋升策略以及年轻代和老年代收集工作的时机和平衡的努力所依据的启发式方法仍在开发中,并正在通过实际工作负载进行测试。同时,为了达到最优性能,可能需要手动调优。