JEP 333: 一个可伸缩的低延迟垃圾收集器

译者:王鸿飞

摘要

Z垃圾收回器,也被称为 ZGC, 是一种可伸缩的低延迟垃圾收集器。

目标

  • 垃圾回收停顿时间不超过10ms
  • 无论是相对小的堆(几百MB)还是大堆(TB级)都能应对自如
  • 与G1相比,吞吐量下降不超过15%
  • 方便日后在此基础上实现新的gc特性、利用colored pointers(译者注:暂时翻译为彩色指针)和读屏障进一步优化收集器
  • 支持Linux/64平台

我们有十足的信心能在各种 workload 中实现上述目标。同时我们也想说,在目前已知的大量 workload 中,实现这些目标并不难。

暂时不打算做的事

我们不打算为非Linux/64平台实现 ZGC。如果开发者对这方面确实有需求的话,以后会考虑添加对其它平台的支持。

动机

垃圾回收一直是Java语言的强项。然而如果垃圾回收停顿时间过长的话,就会对程序响应时间造成负面影响。通过大幅度缩短停顿时间,我们可以让Java适用于更多类型的应用程序。
除此之外,现代计算机中可供应用程序使用的内存越来越多了。开发者们希望JVM能够在更高效的利用大内存的优势的同时又不会出现长时间的停顿。

ZGC描述

大体上来说,ZGC是一种并发的、不分代的、基于Region且支持NUMA的压缩收集器。因为只会在枚举根节点的阶段STW, 因此停顿时间不会随着堆大小或存活对象的多少而增加。

ZGC的一个核心设计就是读屏障与彩色指针(colored object pointers, 缩写, colored oops)组合起来使用(译者注: 彩色指针, 说明见 https://fosdem.org/2018/schedule/event/zgc/attachments/slides/2211/export/events/attachments/zgc/slides/2211/ZGC_FOSDEM_2018.pdf, 总体来说是一种利用64位指针中未使用的bit来保存元数据的指针)。这是ZGC可以与用户线程并发执行的原因。从Java的线程角度来看,读取Java对应中的引用变量的操作属于一种读屏障。与单纯的取对象内存地址相比,使用读屏障可以利用彩色指针中包含的信息来决定在允许Java线程读取指针的地址值之前是否需要执行一些操作。例如,对象可能被垃圾收集器移动过了,这时读屏障就可以感知到这种情况并执行一些必要的行为。

跟其它的可选方案相比,我们认为使用彩色指针模式有一些非常吸引人的优势,比如:

  • 这允许我们在移动对象/整理内存阶段,在指向可回收/重用区域的指针确定之前回收/重用这部分内存。(原文: It allows us to reclaim and reuse memory during the relocation/compaction phase, before pointers pointing into the reclaimed/reused regions have been fixed.)。这有利于降低堆的开销。这同时也意味着我们不需要再实现一个单独的标记-整理算法用于处理Full GC。
  • 这允许我们使用相对来说更少量、更简单的GC屏障。这可以降低JVM运行时的性能开销。同时也可以让JVM字节码解释器和JIT编译器中的GC代码更加容易实现和优化。
  • 我们目前会在彩色指针中保存与标记和重定位相关的数据。不过,只要彩色指针中还有足够的未使用的bit, 我们还可以在里面存储更多对读屏障有用的信息。我们认为这为未来实现更多的特性奠定了良好的基础。比如,在复杂多变的内存环境下,我们可以在彩色指针中存储一些追踪信息来让垃圾回收器在移动对象时能将低频次使用的对象移动到不常访问的内存区域(译者注:原文为 cold storage)。

性能

我们已经使用SPECjbb 2015[1] (译者注:一种benchmark标准) 做了常规性能测试。无论是吞吐量还是延迟数据,结果都看起来很不错。下面是在128G堆下 ZGC 与 G1的对比数据:
(Higher is better)

ZGC
max-jOPS: 100%
critical-jOPS: 76.1%

G1
max-jOPS: 91.2%
critical-jOPS: 54.7%

下面是上述benchmark过程中的GC停顿数据。ZGC做到了全程保持停顿时间在10ms以内的目标。注意,根据具体机器配置和参数的不同,测试结果可能会小范围浮动:
(Lower is better)

ZGC
avg: 1.091ms (+/-0.215ms)
95th percentile: 1.380ms
99th percentile: 1.512ms
99.9th percentile: 1.663ms
99.99th percentile: 1.681ms
max: 1.681ms

G1
avg: 156.806ms (+/-71.126ms)
95th percentile: 316.672ms
99th percentile: 428.095ms
99.9th percentile: 543.846ms
99.99th percentile: 543.846ms
max: 543.846ms

我们还做了一些SPEC其它的benchmark,总体上来说,ZGC完全做到了10ms内的停顿。

局限性

实验版本的ZGC不支持类的卸载。ClassUnloadingClassUnloadingWithConcurrentMark参数默认是关闭的,即便你启动也是不生效的。

同时,ZGC也不支持JVMCI (译者注: Java based JVM compiler interface)。如果开启了EnableJVMCI选项则会在JVM启动时打印出错误信息。

以上这些局限日后都会解决掉。

构建和使用

为了方便起见,试验性的JVM特性默认都是关闭的。因此对于ZGC来说,除非 你在编译JVM时显式指定--with-jvm-features=zgc, 否则ZGC是不会被包含在JVM中的。(在Oracle版本的JVM中,ZGC会被默认集成到JDK的Linux/64版本中)

如果想在运行时使用ZGC,则还需要添加以下参数:

-XX:+unlockExperimentalVMOptions -XX:+UseZGC

其他实现低延迟的方案

  • 一种显而易见的方法是让G1的内存整理阶段可以并发执行。这种方案在很早的阶段就被放弃掉了。原因是现有的G1代码并不是为这种设计而编写的,因此要想在保证稳定的前提下实现这种特性非常困难。
  • 一种方案是对现有的CMS收集器进行改进。然而这并不是一种好的选择,原因有很多,比如CMS不支持内存整理、代码复杂性和CMS实际上已经停止开发等。
  • Shenandoah Project正在探索通过使用Brooks Pointers来实现并发。

猜你喜欢

转载自blog.csdn.net/tracker_w/article/details/80806839
333