跳到主要内容

JEP 404:分代 Shenandoah(实验性)

概括

通过实验性的分代收集功能增强Shenandoah 垃圾收集器,以提高可持续吞吐量、负载峰值弹性和内存利用率。

目标

主要目标是提供一种实验性的分代模式,而不会破坏非分代 Shenandoah。我们打算在未来版本中将分代模式设为默认模式。

其他目标是与非代际 Shenandoah 相关的:

  • 在不牺牲低 GC 暂停的情况下减少持续的内存占用。

  • 减少 CPU 和电源的使用。

  • 降低分配高峰期间发生退化和完整收集的风险。

  • 维持高吞吐量。

  • 继续支持压缩对象指针。

  • 最初支持 x64 和 AArch64,随着此实验模式逐渐准备就绪,将添加对其他指令集的支持作为默认选项。

非目标

  • 我们的目标并不是取代非代际的 Shenandoah,它将继续作为默认的操作模式,其性能或功能不会有任何退步。

  • 我们的目标并不是提高所有可想象的工作负载的性能。分代系统将根据需要动态调整以接近非分代系统,但对于某些工作负载,从单一代开始并保持单一代可能仍然是更好的选择。尽管如此,我们预计大多数用例都能从分代收集中受益。

  • 与传统的 stop-the-world GC 相比,改进 CPU 和功耗并不是目标。如果可以容忍较长的暂停时间,其他收集器(如 G1)可能仍会提供更节能的行为。分代 Shenandoah 只能近似但永远无法与 stop-the-world GC 的效率策略相媲美,因为它的任务是将暂停时间保持在更低水平,并完全避免 stop-the-world 压缩。然而,在这方面,分代 Shenandoah 将比非分代 Shenandoah 更接近当前的分代 stop-the-world GC。

  • 最大化变量吞吐量并不是我们的目标。如果可以容忍较长的暂停时间,其他收集器(例如并行收集器)可能仍会在某些平台上提供更高的吞吐量。

  • 在初始版本中,人体工程学启发法可能无法在所有工作负载上提供最佳行为。

成功指标

  • 使用SPECjbb2015HyperAllocExtrememDacapo对分代 Shenandoah 与非分代 Shenandoah 进行基准测试。

  • HyperAlloc、Extremem 和类似工作负载的运行范围(即分配率、堆占用率和暂停时间目标的组合)与非分代 Shenandoah 进行了比较。成功的运行减少或消除了分配停顿的次数以及对完整或退化收集的需求。

动机

具有并发压缩功能的垃圾收集器能够将 GC 暂停时间完全融入其他常见 JVM 暂停的个位数毫秒范围内,同时几乎不受变量执行速度的限制。非分代 Shenandoah 垃圾收集已经为延迟敏感的 Java 应用程序提供了这种理想的 GC 行为。但是,它只能在有限的操作范围内实现这一点(即堆占用率和分配率的组合)。

最小化平均 GC 成本的经典方法是采用代际假设,即大多数对象在年轻时死亡,并将周期集中在处理年轻且大多数死亡的对象上。与代际收集器 G1、CMS 和 Parallel 相比,非代际 Shenandoah 往往需要更多的堆空间,并且更努力地恢复无法访问的对象占用的空间。

基于区域的分代收集器能够根据对象人口统计的变化动态调整其代大小和复制策略,从而使收集器能够调整不遵循分代假设的工作负载。即使幸存对象在年轻代中的复制频率高于必要频率,与非分代收集器相比,这种成本通常与标记长寿命对象的频率降低相比微不足道。

并发收集器也是分代的,并且可以动态调整其年轻代的大小和相关操作参数,既可以实现较低的暂停时间,又可以在其他性能方面保持竞争力。

描述

Shenandoah 垃圾收集器的这一增强功能将 Java 堆分为两代。与其他分代收集器一样,GC 工作重点放在年轻代,即变量分配器在其中进行分配并且可以用较少的努力回收短暂对象的一代。我们建议采用以下方法进行初步实施。

在每个代上运行的收集算法都与非分代 Shenandoah 密切相关。在年轻代中,分代 Shenandoah 使用与传统 Shenandoah 相同的启发式算法来区分保存新分配对象的内存区域和保存最近一次或多次年轻代收集中幸存下来的对象的内存区域。

每个代由 Shenandoah 堆区域的子集组成。在任何给定时间,一个区域被视为空闲或专用于年轻代或老代。每个代的大小由其占用的区域加上空闲区域的配额决定。可以容忍超出各自其他代的空闲配额,但这会加速收集触发,并可能导致退化和完整收集。我们正在积极改进算法,以控制收集阶段调度、年轻代大小、老年代龄和其他自动调整机制。

Shenandoah 具有独特的加载引用屏障 (LRB),支持 32 位版本和 64 位版本中的压缩 32 位对象指针(“压缩 oops”)。为了限制对变量的影响,我们对两代使用相同的 LRB,不做任何更改,并使用单个清除器进行老年代和年轻年代的收集工作。典型的清除阶段会专门从年轻年代区域或年轻年代和老年代区域的组合中收集垃圾。此行为模仿 G1 的年轻年代和混合年代收集。与 G1 相比,主要的改进是分代 Shenandoah 的年轻年代和混合年代收集与变量器同时进行。

特定代的标记阶段在很大程度上是相互解耦的。在年轻代标记和撤离多次发生期间,并发老一代标记在后台进行。可以抢占老一代标记来执行优先级更高的年轻代收集。一旦老一代标记完成,后续撤离和引用更新将包括老一代区域,直到整个老一代收集集都已处理完毕。

对于记忆集的实现,我们使用从并行和 CMS GC 实现中借用的现有卡标记代码,并用新代码进行补充,以允许记忆集扫描与变量执行同时运行。

非分代 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 和 Linux 上的 AArch64。SAP 已将代际模式移植到 PowerPC 并在该平台上进行了测试。CI 测试在 Linux、macOS 和 Windows 上运行。其他平台的代际模式支持可以稍后实现和优化。

风险和假设

  • 记忆集操作,特别是扫描,可能会增加暂停时间。

  • 与记忆集相关的障碍可能会增加变量器的开销。

  • 自动配置代大小、对象提升策略、时间以及平衡年轻代和老代收集的启发式方法仍在开发中,并且正在通过实际工作负载进行测试。同时,可能需要手动调整才能获得最佳性能。