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