JEP 439:世代 ZGC
概括
通过扩展 Z 垃圾收集器 ( ZGC ) 来维护年轻对象和老对象的不同代,从而提高应用程序性能。这将使 ZGC 能够更频繁地收集年轻对象(这些对象往往会在年轻时死亡) 。
目标
使用 Generational ZGC 运行的应用程序应该享受
- 降低分配停滞的风险,
- 降低所需的堆内存开销,并且
- 降低垃圾收集 CPU 开销。
与非分代 ZGC 相比,这些好处应该不会显着降低吞吐量。应保留非代 ZGC 的基本属性:
- 暂停时间不应超过1毫秒,
- 应支持从几百兆字节到数 TB 的堆大小,并且
- 应该需要最少的手动配置。
作为最后一点的例子,应该不需要手动配置
- 一代人的规模,
- 垃圾收集器使用的线程数,或
- 对象应该在年轻代中驻留多长时间。
最后,对于大多数用例来说,分代 ZGC 应该是比非分代 ZGC 更好的解决方案。我们最终应该能够用前者取代后者,以减少长期维护成本。
非目标
在年轻代中执行参考处理并不是目标。
动机
ZGC(JEP 333)专为低延迟和高可扩展性而设计。自 JDK 15 ( JEP 377 )起,它就可供生产使用。
ZGC 在应用程序线程运行时完成大部分工作,仅短暂暂停这些线程。 ZGC 的暂停时间始终以微秒为单位进行测量;相比之下,默认垃圾收集器 G1 的暂停时间范围从毫秒到秒。 ZGC 的低暂停时间与堆大小无关:工作负载可以使用从几百兆字节一直到数 TB 的堆大小,并且仍然享有较低的暂停时间。
对于许多工作负载来说,简单地使用 ZGC 就足以解决与垃圾收集相关的所有延迟问题。只要有足够的资源(即内存和 CPU)可确保 ZGC 回收内存的速度快于并发运行的应用程序线程消耗内存的速度,这种方法就可以很好地发挥作用。然而,ZGC 目前将所有对象存储在一起,无论年龄如何,因此它每次运行时都必须收集所有对象。
弱世代假说指出,年轻的物体往往会早逝,而老的物体往往会留下来。因此,收集年轻对象需要更少的资源并产生更多的内存,而收集旧对象需要更多的资源并产生更少的内存。因此,我们可以通过更频繁地收集年轻对象来提高使用 ZGC 的应用程序的性能。
描述
启用分代ZGC
为了确保顺利继承,我们首先将分代 ZGC 与非分代 ZGC 一起使用。命令-XX:+UseZGC
行选项将选择非分代ZGC;要选择 Generational ZGC,请添加-XX:+ZGenerational
选项:
$ java -XX:+UseZGC -XX:+ZGenerational ...
在未来的版本中,我们打算将分代 ZGC 设置为默认值,届时-XX:-ZGenerational
将选择非分代 ZGC。在以后的版本中,我们打算删除非分代 ZGC,届时该ZGenerational
选项将变得过时。
设计
分代 ZGC 将堆分为两个逻辑_代_:_年轻_代用于最近分配的对象,而_老_一代用于长期存在的对象。每一代的收集都是独立的,因此 ZGC 可以专注于收集有利可图的年轻对象。
与非分代 ZGC 一样,所有垃圾收集都是在应用程序运行时同时完成的,应用程序暂停时间通常短于一毫秒。由于 ZGC 与应用程序同时读取和修改对象图,因此必须注意为应用程序提供一致的对象 图视图。 ZGC 通过彩色指针、加载屏障和存储屏障来实现这一点。
-
_彩色指针_是指向堆中对象的指针,该对象与对象的内存地址一起包含对对象的已知状态进行编码的元数据。元数据描述了对象是否已知是活动的、地址是否正确等等。 ZGC 始终使用 64 位对象指针,因此可以容纳高达数 TB 的堆的元数据位和对象地址。当一个对象中的字段引用另一个对象时,ZGC 使用彩色指针实现该引用。
-
负载_屏障_是 ZGC 在应用程序读取引用另一个对象的对象字段时注入到应用程序中的代码片段。负载屏障解释存储在字段中的彩色指针的元数据,并可能在应用程序使用引用的对象之前采取一些操作。
非分代 ZGC 同时使用彩色指针和负载屏障。分代 ZGC 还使用存储屏障来有效地跟踪从一代中的对象到另一代中的对象的引用。
- 存储_屏障_是由 ZGC 注入到应用程序中的代码片段,无论应用程序将引用存储到对象字段中。分代 ZGC 向彩色指针添加新的元数据位,以便存储屏障可以确定正在写入的字段是否已被记录为可能包含代间指针。彩色指针使世代 ZGC 的存储屏障比传统的世代存储屏障更加高效。
存储屏障的添加允许世代 ZGC 将标记可达对象的工作从加载屏障转移到存储屏障。也就是说,存储屏障可以使用彩色指针中的元数据位来有效地确定在存储之前是否需要标记该字段先前引用的对象。
将标记移出负载屏障可以更轻松地优化它们,这一点很重要,因为负载屏障通常比存储屏障更频繁地执行。现在,当负载屏障解释彩色指针时,如果对象被重新定位,它只需要更新对象地址,并更新元数据以表明该地址已知是正确的。后续的加载屏障将解释此元数据,并且不再检查对象是否已重新定位 。
分代 ZGC 在彩色指针中使用不同的标记和重定位元数据位集,以便可以独立收集各代。
其余部分描述了区分分代 ZGC 与非分代 ZGC 以及其他垃圾收集器的重要设计概念:
- 没有多重映射内存
- 优化障碍
- 双缓冲记忆集
- 无需额外堆内存的重定位
- 密集堆区域
- 大物体
- 完整的垃圾收集
没有多重映射内存
非分代 ZGC 使用多映射内存来减少负载屏障的开销。相反,分代 ZGC 在加载和存储屏障中使用显式代码。
对于用户来说,这一更改的主要优点是可以更轻松地测量堆所使用的内存量。使用多重映射内存,相同的堆内存被映射到三个独立的虚拟地址范围,因此诸如此类的工具报告的堆使用情况ps
大约是实际使用的内存量的三倍。
对于 GC 本身,此更改意味着彩色指针中的元数据位不再需要位于与堆的可访问内存地址范围相对应的指针部分中。这允许添加更多元数据位,并且还可以将最大堆大小增加到超出非分代 ZGC 的 16 TB 限制。
在分代 ZGC 中,存储在对象字段中的对象引用被实现为彩色指针。然而,存储在 JVM 堆栈中的对象引用在硬件堆栈或 CPU 寄存器中实现为_无色_指针,没有元数据位。加载和存储屏障在有色和无色指针之间来回转换。
由于彩色指针永远不会出现在硬件堆栈或 CPU 寄存器中,因此只要可以有效地完成彩色指针和无色指针之间的转换,就可以使 用更奇特的彩色指针布局。 Generational ZGC 使用的彩色指针布局将元数据放在指针的低位中,将对象地址放在高位中。这最大限度地减少了负载屏障中的机器指令数量。通过仔细编码内存地址和元数据位,单个移位指令(在 x64 上)既可以检查指针是否需要处理,也可以删除元数据位。
优化障碍
随着存储屏障和加载屏障的新职责的引入,更多的 GC 代码将与已编译的应用程序代码混合在一起。为了最大限度地提高吞吐量,屏障需要高度优化。 Generational ZGC 的许多关键设计决策都涉及彩色指针方案和障碍。
用于优化障碍的一些技术是:
- 快路径和慢路径
- 最大限度地减少负载屏障的责任
- 记住设置的障碍
- SATB 标记障碍
- 融合商店屏障检查
- 存储屏障缓冲区
- 修补障碍
快路径和慢路径
ZGC 将障碍分为两部分。快速_路径_检查在应用程序使用引用的对象之前是否必须完成一些额外的 GC 工作。_慢速路径_会完成额外的工作。所有对象访问都运行快速路径检查。顾名思义,它需要速度快,因此该代码被直接插入到即时编译的应用程序代码中。慢速路径只占用一小部分时间。当采取慢速路径时,所访问的对象指针的颜色会发生变化,以便后续对同一指针的访问在一段时间内不会再次触发慢速路径。因此,高度优化慢速路径并不那么重要。为了可维护性,它们在 JVM 中作为 C++ 函数实现。
在非分代 ZGC 中,这就是负载屏障的划分方式。在世代 ZGC 中,相同的方案应用于存储屏障及其相关的 GC 工作。
最大限度地减少负载屏障的责任
在非分代 ZGC 中,负载屏障负责
- 更新 GC 重新定位的对象的陈旧指针
- 将加载的对象标记为活动的——应用程序正在加载该对象,因此它被认为是活动的。
在 Generational ZGC 中,我们必须跟踪两代并在有色和无色指针之间进行转换。为了最小化复杂性,并允许优化加载屏障快速路径,标记的责任转移到存储屏障。
在世代 ZGC 中,负载屏障负责
- 从彩色指针中删除元数据位并
- 更新 GC 重新定位的对象的过时指针。
商店屏障负责
- 添加元数据位以创建彩色指针,
- 维护_记忆集_,跟踪老一代到年轻一代的对象指针,以及
- 将对象标记为活着的。
记住设置的障碍
Generational ZGC收集年轻代时,只访问年轻代中的对象。然而,老年代中的对象可以包含指向年轻代中的对象的字段,即老年代到年轻代的指针。由于两个原因,必须在年轻一代收集期间访问这些字段。
-
GC 标记根——这样的字段可以包含唯一的引用,使年轻代对象图的一部分保持可达。 GC 必须将这些字段视为对象图的根,以确保找到所有活动对象并将其标记为活动。
-
老年代中的陈旧指针——收集年轻代会移动对象,但指向这些对象的指针不会立即更新。相反,当应用程序遇到指针时,负载屏障会延迟更新指针。在某些时候,GC 必须更新应用程序未遇到的任何过时的老到年轻代指针。
老一代到年轻代指针的集合称为_记忆集_。记忆集包含老年代中的所有内存地址,这些地址可能包含指向年轻代中对象的指针。存储屏障将条目添加到记忆集中。每当一个引用被存储到一个对象字段中时,它就被认为可能包含一个老到年轻代指针。存储屏障慢速路径将存储过滤到年轻代中的字段,因为只有老一代中的地址才有意义。慢速路径不会根据写入该字段的值进行过滤,该值可能是指年轻代或老一代的值。 GC 在使用记忆集时会检查对象字段的当前值。
所有这些都确保了存储屏障在维护记忆集方面的_一次性属性_。这意味着,在两个连续的年轻代标记阶段开始之间,每个存储到对象字段仅采用一次存储屏障慢速路径。第一次写入字段时,会发生以下步骤:
- 快速路径检查存储到字段的要覆盖的值,
- 颜色表明自上一个年轻代标记阶段以来该字段尚未被写入,因此
- 采取的是缓慢的路径,
- 存储到字段的地址被添加到记忆集中,并且
- 新的指针值被着色并存储在字段中。
新的指针值以这样的方式着色,以便后续的快速路径检查将看到该对象字段已经通过了慢速路径。