JDK21新特性之分代 ZGC

一、概述

分代ZGC(Generational ZGC)为年轻和年老的对象保留不同的世代,从而提高应用程序的性能。这将使 ZGC 能够更频繁地收集年轻对象,因为年轻对象往往在很年轻时就会死亡。

ZGC 从 JDK 15 开始就可用于生产,其设计旨在实现低延迟和高可扩展性。它的大部分工作都是在应用程序线程运行时进行的,只在短时间内暂停这些线程。ZGC 的暂停时间始终以微秒为单位,因此成为要求低延迟和高可扩展性的工作负载的首选。

分代 ZGC 最初将与非分代 ZGC 同时提供。用户可以通过在 -XX:+UseZGC 命令行选项中添加 -XX:+ZGenerational 选项来选择分代 ZGC。在未来的版本中,分代 ZGC 将成为默认选项,最终非分代 ZGC 将被移除。

二、设计目标

使用新一代 ZGC 运行的应用程序应享有

  • 降低分配停滞的风险、
  • 降低堆内存开销
  • 降低垃圾回收 CPU 开销。

与非分代 ZGC 相比,这些优势不应导致吞吐量的显著降低。应保留非分代 ZGC 的基本特性:

  • 暂停时间不应超过 1 毫秒
  • 支持从几百兆字节到几千兆字节的堆大小
  • 以及应尽量减少手动配置

以最后一点为例,应无需手动配置

  • 代的大小
  • 垃圾回收器使用的线程数
  • 或对象应在年轻代中停留多长时间

最后,与非分代ZGC相比,分代ZGC在大多数使用情况下都是更好的解决方案。我们最终应该能用分代 ZGC取代非分代 ZGC,以降低长期维护成本。

三、设计动机

ZGC 的大部分工作都是在应用线程运行时完成的,仅短暂暂停这些线程。ZGC 的暂停时间一直以微秒为单位;相比之下,默认垃圾回收器 G1 的暂停时间从毫秒到秒不等。ZGC 的低暂停时间与堆大小无关: 工作负载使用的堆大小可以从几百兆字节一直到多 TB,而且暂停时间仍然很短。

对于许多工作负载来说,只需使用 ZGC 就足以解决与垃圾回收相关的所有延迟问题。只要有足够的可用资源(即内存和 CPU),确保 ZGC 回收内存的速度快于并发运行的应用线程消耗内存的速度,这种方法就能很好地发挥作用。但是,ZGC 目前将所有对象存储在一起,而不考虑其年龄,因此每次运行时都必须收集所有对象。

弱代际假说认为,年轻的对象往往死得早,而年老的对象则会一直存在。因此,收集年轻对象需要的资源更少,产生的内存也更多,而收集老对象需要的资源更多,产生的内存也更少。因此,我们可以通过更频繁地收集年轻对象来提高使用 ZGC 的应用程序的性能。

四、说明

4.1. 启用分代 ZGC

为确保顺利接替,将同时提供分代 ZGC 和非分代 ZGC。命令行选项 -XX:+UseZGC 将选择非分代 ZGC;要选择分代 ZGC,请添加 -XX:+ZGenerational 选项:

$ java -XX:+UseZGC -XX:+ZGenerational ...

在未来的版本中,我们打算将分代 ZGC 设为默认值,届时 -XX:-ZGenerational 将选择非分代 ZGC。在更晚的版本中,我们打算删除非分代 ZGC,届时 ZGenerational 选项将变得过时。

4.2. 设计

分代 ZGC 将堆分成两个逻辑世代: 年轻一代用于最近分配的对象,而老一代用于长期存在的对象。每一代的垃圾收集都是独立进行的,因此 ZGC 可以专注于收集有利可图的年轻对象。

与非分代 ZGC 一样,所有垃圾收集工作都是在应用程序运行时并发进行的,应用程序运行暂停时间通常短于一毫秒。由于 ZGC 与应用程序同时读取和修改对象图,因此必须注意为应用程序提供一致的对象图视图。ZGC 通过彩色指针、加载障碍和存储障碍来做到这一点。

  • 彩色指针(colored pointer)是指向堆中对象的指针,它与对象的内存地址一起,包含了编码对象已知状态的元数据。元数据描述了对象是否已知存活、地址是否正确等。ZGC 始终使用 64 位对象指针,因此可以容纳多达数 TB 堆的元数据位和对象地址。当一个对象中的字段指向另一个对象时,ZGC 会使用彩色指针来实现这种指向。
  • 加载屏障( load barrier)是 ZGC 向应用程序注入的代码片段,只要应用程序读取对象中指向另一个对象的字段,加载屏障就会注入到应用程序中。加载障碍会解释存储在字段中的彩色指针的元数据,并有可能在应用程序使用引用对象之前采取一些措施。

非分代 ZGC 同时使用彩色指针和加载屏障。世代 ZGC 还使用存储屏障来有效跟踪从一代对象到另一代对象的引用。

  • 存储屏障(store barrier)是 ZGC 注入应用程序的代码片段,只要应用程序将引用存储到对象字段中,就会使用存储屏障。一代 ZGC 为彩色指针添加了新的元数据位,这样存储屏障就能确定正在写入的字段是否已被记录为可能包含跨代指针。彩色指针使世代 ZGC 的存储屏障比传统的世代存储屏障更高效。

增加存储屏障后,世代 ZGC 可以将标记可达对象的工作从加载屏障转移到存储屏障。也就是说,存储障碍可以使用彩色指针中的元数据位来有效地确定,在存储之前,字段所引用的对象是否需要标记。

将标记移出加载障碍后,可以更容易地对其进行优化,这一点非常重要,因为加载障碍的执行频率往往高于存储障碍。现在,当加载障碍解释着色指针时,如果对象被重新定位,它只需更新对象地址,并更新元数据,以表明已知地址是正确的。后续的加载障碍将解释此元数据,而不再检查对象是否已被重新定位。

分代 ZGC 在彩色指针中使用不同的标记和重定位元数据位集,因此可以独立收集分代数据。

下面简要介绍分代式 ZGC 与非分代式 ZGC 以及其他垃圾回收器的重要设计理念:

  • 无多映射内存(No multi-mapped memory)
    • 在一代 ZGC 中,存储在对象字段中的对象引用是以彩色指针的形式实现的。而存储在 JVM 堆栈中的对象引用则是无色指针,没有元数据位,存储在硬件堆栈或 CPU 寄存器中。加载屏障和存储屏障在有颜色和无颜色指针之间来回转换。
    • 由于彩色指针从不出现在硬件堆栈或 CPU 寄存器中,因此只要能高效地完成彩色指针和无色指针之间的转换,就可以使用更奇特的彩色指针结构。分代 ZGC 使用的彩色指针结构将元数据放在指针的低阶位,将对象地址放在高阶位。这样可以最大限度地减少加载屏障中的机器指令数量。只要对内存地址和元数据位进行仔细编码,一条移位指令(在 x64 上)就能检查指针是否需要处理,并移除元数据位。
  • 优化的屏障(Optimized barriers)
    • 随着存储屏障和加载屏障新职责的引入,更多的 GC 代码将与编译后的应用代码混合在一起。为了最大限度地提高吞吐量,需要对障碍进行高度优化。新一代 ZGC 的许多关键设计决策都涉及彩色指针方案和障碍。
    • 用于优化屏障的一些技术包括:
      • 快速路径和慢速路径(Fast paths and slow paths)
      • 最小化负载屏障责任(Minimizing load barrier responsibilities)
      • 记忆集屏障(Remembered-set barriers)
      • SATB 标记屏障(SATB marking barriers)
      • 融合存储屏障检查(Fused store barrier checks)
      • 存储屏障缓冲区(Store barrier buffers)
      • 屏障修补(Barrier patching)
  • 双缓冲记忆集(Double-buffered remembered sets)
    • 许多 GC 使用一种称为卡片表标记的记忆集技术来跟踪代际指针。当应用线程写入对象字段时,也会写入(即弄脏)一个称为卡表的大型字节数组中的一个字节。通常,表中的一个字节对应堆中 512 字节的地址范围。要找到所有从老一代到年轻一代的对象指针,GC 必须找到并访问地址范围内的所有对象字段,这些地址范围与卡表中的脏字节相对应。
    • 相比之下,分代 ZGC 使用位图精确记录对象字段位置,位图中的每个位都代表一个潜在的对象字段地址。每个旧一代区域都有一对记忆设置位图。其中一个位图处于活动状态,由运行存储障碍的应用程序线程填充,而另一个位图则被 GC 用作所有记录的老一代对象字段的只读副本,这些字段可能指向年轻一代中的对象。每次启动年轻一代收集时,这两个位图都会原子交换。这种方法的一个好处是,应用程序线程无需等待位图被清除。在应用线程同时填充另一个位图时,GC 会处理并清除其中一个位图。这样做的另一个好处是,由于应用程序线程和 GC 线程可以在不同的位图上工作,因此无需在两类线程之间设置额外的内存屏障。其他使用卡片表标记的分代收集器(如 G1)在标记卡片时需要内存栅栏,从而可能导致存储障碍性能下降。
  • 无需额外堆内存的重定位(Relocations without additional heap memory)
    • 其他 HotSpot GC 中的年轻一代集合使用清道夫模型,即一次性找到并重新定位存活对象。在 GC 完全了解哪些对象还活着之前,年轻一代中的所有对象都必须重新定位。使用这种模式的 GC 只能在所有对象都被重新定位后才能回收内存。因此,这些 GC 需要猜测存活对象所需的内存量,并确保 GC 启动时上述内存量可用。如果猜测错误,就需要进行更昂贵的清理操作;例如,对未重新定位的对象进行就地钉扎,这将导致碎片化,或者在所有应用线程停止的情况下进行完全 GC。
    • 分代 ZGC 采用两次传递: 第一次访问并标记所有可到达的对象,第二次重新定位标记的对象。由于 GC 在重定位阶段开始前就掌握了完整的有效性信息,因此可以按区域粒度划分重定位工作。一旦一个区域中的所有有效对象都被重新定位,即该区域已被疏散,该区域就可以作为新的目标区域被重新使用,用于重新定位或应用线程的分配。即使没有空闲区域可供重新定位对象,ZGC 仍可继续将对象压缩到当前已重新定位的区域中。这样,分代 ZGC 就能在不使用额外堆内存的情况下,重新定位并压缩年轻一代。
  • 密集堆区域(Dense heap regions)
    • 在将对象迁出年轻一代时,不同区域的实时对象数量及其占用的内存量将有所不同。例如,最近分配的区域可能包含更多的实时对象。
    • ZGC 会分析年轻一代区域的密度,以确定哪些区域值得疏散,哪些区域太满或疏散成本太高。未被选中疏散的区域将就地老化: 这些区域中的物体将留在原处,而这些区域要么作为幸存者区域保留在年轻一代区域中,要么晋升为老一代区域。存活区域中的物体有第二次死亡的机会,希望在下一次年轻一代收集开始时,已经有足够多的物体死亡,使更多的区域有资格撤离。
    • 这种让密集区域就地老化的方法可以减少收集年轻一代所需的工作量。
  • 大型对象(Large objects)
    • ZGC 已经能很好地处理大型对象。通过将虚拟内存与物理内存解耦并过度保留虚拟内存,ZGC 通常可以避免碎片问题,而这些问题有时会导致在使用 G1 时难以分配大型对象。
    • 在分代 ZGC 中,我们更进一步,允许在年轻一代中分配大型对象。由于可以在不重新定位的情况下对区域进行老化,因此没有必要为了防止昂贵的重新定位而在老一代中分配大型对象。相反,如果大型对象的寿命较短,则可以将其收集到年轻一代中;如果大型对象的寿命较长,则可以以较低的成本将其提升到老一代中。
  • 完全垃圾回收(Full garbage collections)
    • 收集旧一代对象时,年轻一代对象中会有指向旧一代对象的指针。这些指针被视为老一代对象图的根。年轻一代中的对象经常发生变化,因此不会跟踪年轻一代到老一代的指针。相反,这些指针是通过在旧一代标记阶段运行年轻一代收集来找到的。当年轻代收集发现指向老一代的指针时,就会将其传递给老一代标记进程。
    • 这个额外的年轻代收集仍会像正常的年轻代收集一样执行,并在存活区域中留下存活对象。这样做的一个结果是,年轻一代中的存活对象在收集旧一代时不会受到引用处理和类卸载的影响。例如,一个应用程序释放了对象图的最后一个引用,调用了 System.gc(),然后期待某些弱引用被清除或排队,或者某些类被卸载,这时就会出现这种情况。为了缓解这种情况,当应用程序代码明确要求进行 GC 时,就会在开始旧一代收集之前,先进行一次额外的年轻一代收集,以便将所有存活对象提升到旧一代。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

markvivv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值