从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队

发布时间:2023-04-27 09:21:13

作者:京东科技 康志兴

Shenandoah

Shenandoah这个词来自印第安语。20世纪40年代,一首著名的航海歌曲在水手中广为流传,讲述了一位年轻富商爱上印第安酋长Shenandoah女儿的故事。 后来美国有一条以Virginia州西部命名的小河,所以Shenandoah的中文译名为“情人渡”。

Openenandoah首次出现 在JDK12中,Red Hat开发主要是为了解决各种垃圾回收器在处理大量垃圾时停顿较长的问题。

与G1相比,Shenandoah将低停顿降至10ms,与堆大小无关。它的设计非常激进,许多设计点更倾向于低停顿而不是高吞吐量。

“G1继承者”

Shenandoah是OpenJDK中的垃圾处理器,但与Oracle相比 JDK中根正苗红的ZGC,Shenandoah可以说更像是G1的继承者,在很多方面与G1非常相似,甚至共享了一些代码。

一般来说,Shenandoah和G1有三个主要区别:

1.G1回收需要STW,这部分停顿占整体停顿时间的80%以上,而Shenandoah实现并发回收。

2.Shenandoah不再区分年轻代和年老代。

3.Shenandoah用连接矩阵代替G1中的卡表。

请看前一篇关于G1的详细介绍:从原理上谈JVM(2):从串行收集器到分区收集开创者G1

连接矩阵(Connection Matrix)

G1中的每个Region都需要维护卡表,这不仅消耗了计算资源,而且占据了很大的内存空间。Shenandoah使用连接矩阵来优化这个问题。

如果Regionn,连接矩阵可以简单地理解为二维表格 A中有一个对象指向Region B中的对象,然后在表格的第A行第B列上标记。

比如,Region 1指向Region 3,Region 4指向Region 2,Region 3指向Region 5:

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_垃圾回收器

与G1记忆集相比,连接矩阵的粒度更厚,直接指向整个Region,因此扫描范围更大。然而,由于GC是并发的,这是一个通过选择资源消耗较低的连接矩阵来妥协吞吐量的决定。

转发指针转发指针的性能优势

要实现并发回收,需要在用户线程运行的同时,将生存对象逐渐复制到空Region中,新旧对象同时存在于堆中。那么如何让用户线程访问新对象呢?

在此之前,保护陷阱通常设置在旧对象的原始内存上(Memory Protection Trap),当访问旧对象时,程序进入预设的异常处理器,然后通过处理器中的代码将访问转发到复制的新对象。

自陷是通过线程发起来打断当前执行程序,然后获得CPU的使用权。该操作通常需要操作系统的参与,因此将用户状态转换为核心状态,成本非常巨大。

所以Rodney A.Brooks提出了使用转发指针通过旧对象访问新对象的方法:在对象头前添加一个新的引用字段,指向自己,并在生成新对象后指向新对象。然后,当访问对象时,您需要访问转发指针,以查看转发指针的方向。虽然与内存自陷方案相比,也需要更多的访问和转发费用,但前者的消耗要小得多。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_JVM_02

转发指针的问题

转发指针有两个主要问题:线程安全问题和高频访问性能问题。

1.对象体增加了一个转发指针,该指针的修改和对象本身的修改存在线程安全问题。如果通过访问复制新对象,旧对象的修改可能发生在转发对象修改之前,则存在两个对象不一致的问题。对于这个问题,Shenandoah通过CAS操作来确保修改的正确性。

2.转发指针的添加需要覆盖所有对象访问的场景,包括阅读、写作、锁定等,因此需要同时设置阅读屏障和写作屏障。特别是阅读操作比简单的写作操作更频繁,因此高频操作带来的性能问题有很大的影响。因此,Shenandoah在JDK13中优化了这一点,将内存屏障模型改为引用访问屏障,即只引用对象中的读写类型来增加屏障,而不管原始对象的操作如何,从而节省了大量的对象访问操作。

Shenandoah的操作步骤
  1. 初始标记(Init Mark)\[STW\] [同G1\]

标记与GC 直接关联Roots的对象。

  1. 并发标记(Concurrent Marking)[同G1\]

遍历对象图,标记所有可到达对象。

  1. 最终标记(Final Mark)\[STW\] [同G1\]

处理剩余的SATB扫描,并在此阶段统计回收价值最高的Region,将这些Region构成一组回收集。

  1. 并发清理(Concurrent Cleanup)

回收所有不包含任何存活对象的Region(这种Region称为Immediateteon) Garbage Region)。

  1. 并发回收(Concurrent Evacuation)

将回收集中的库存对象复制到其他未使用的库存中。并发复制生存对象,同一对象在堆中同时存在两份,因此对象的读写一致性存在问题。Shenandoah通过使用转发指针将旧对象的请求指向新对象来解决这个问题。这也是Shenandoah和其他GC之间最大的区别。

  1. 初始引用更新(Init Update References)\[STW\]

并发回收后,所有指向旧对象的引用都需要纠正到新对象上。现阶段没有实际操作,只是设置了一个屏障点,以确保上述并发操作已经完成。

  1. 并引用更新(Concurrent Update References)

沿内存物理地址线性遍历堆空间更新并发回收阶段复制对象的引用。

  1. 最后引用更新(Final Update References)\[STW\]

堆叠空间中的引用更新完成后,GC需要修改 在Roots中引用。

  1. 并发清理(Concurrent Cleanup)

此时,回收集中的Region应该全部变成Immediateiate Garbage Region,再次进行并发清理,将这些Region全部回收。

ZGC

ZGC是Oracle官方研发和JDK11引进的,并在JDK15中作为生产就绪使用,在设计之初就定义了三个目标:

1.支持TB级内存

2.停顿控制在10ms以内,不会随着堆积大小的增加而增加

3.对程序吞吐量的影响小于15%

随着JDK的迭代,ZGC在JDK16及以上版本中可以实现不超过1毫秒的停顿,适用于8MB到16TB之间的堆叠尺寸。

ZGC内存布局

ZGC和G1也采用了不同区域的堆内存布局,不同之处在于ZGCRegion(官方称为Page,概念与G1相同的Region)可以动态创建和销毁,容量也可以动态调整。

ZGC的Region分为三种:

1.小Region容量固定为2MB,用于存储小于256KB的对象。

2.中型Region容量固定为32MB,用于存储大于等于256KB但不足4MB的对象。

3.大型Region的容量是2MB的整数倍,存储4MB或以上的对象,每个大型Region中只存储一个大对象。由于大对象的移动成本太高,对象不会被重新分配。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_Shenandoah_03

重分配集(Relocation Set)

G1中的回收集用于存储所有需要G1扫描的Region,而ZGC则用于节省卡表的维护和标记过程将扫描所有Region。如果确定Region中的生存对象需要重新分配,则将Region放入重新分配集中。

一般来说,如果GC分为两个主要阶段:标记和回收,则回收集用于确定标记的Region和重分配集用于确定回收的Region。

染色指针

和Shenandoah一样,ZGC也实现了并发回收。区别在于前者是通过转发指针实现的,后者是通过染色指针技术实现的。

三色标记本质上与对象无关,只与引用有关:通过引用关系确定对象是否存活。不同的垃圾回收器在Hotspot虚拟机中有不同的处理方法,有些标记在对象头中,有些标记在单独的数据结构中,ZGC直接标记在指针上。

64位机器指针为64位,Linux下64位中高18位不能用于寻址,其余46位ZGC选择其中4位用于辅助GC工作,另外42位可支持4T最大内存,一般来说,4T内存完全足够。

具体来说,ZGC在指针中增加了四个标志位,包括FinalizableRemappedMarked 0Marked 1

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_垃圾回收器_04

源码注释如下:

6                 4 4 4  4 4                                             0 3                 7 6 5  2 1                                             0+-+-+-+-+ 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 1111111|++-+-+-+                   | |    ||                   | |    * 41-0 Object Offset (42-bits, 4TB address space)|                   | ||                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0|                   |                                 0010 = Marked1|                   |                                 0100 = Remapped|                   |                                 1000 = Finalizable|                   ||                   * 46-46 Unused (1-bit, always zero)|* 63-47 Fixed (17-bits, always zero)

Finalizable标识表示对象是否只能通过finalize()访问方法,RemappedMarked 0Marked 1用作三色标记(以下简称三色标记)M0M1)。

为什么既有M0还有M1呢?

由于ZGC标记完成后,下一个垃圾回收循环可以在不等待对象指针重映射的情况下进行,也就是说,两个垃圾回收的整个过程是重叠的,因此两个标记位被用作两个相邻GC过程的标记,M0M1交替使用。

染色指针在GC过程中的作用

我们用红、蓝、黄三种颜色表示三种标记状态:

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_JVM_05

1.第一次标记开始时,所有指针都在Remapped状态

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_ZGC_06

  1. 从GC Root开始,沿着对象图扫描,生存对象标记为M0

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_垃圾回收器_07

  1. 标记完成后,开始并发重分配。最终目标是A、B、C三个存活对象都移动到新的Region中。

在整个标记过程中,新分配的对象直接标记为M0,如对象D。

对于复制的对象,指针可以从M0改为Remaped,并将旧对象保存到新对象到映射关系。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_JVM_08

  1. 如果此时系统访问对象C,将触发读取屏障,将原始引用修改到新对象C的地址,并转发访问,最后删除转发记录。

这种行为被称为指针“自愈”。

事实上,如果没有对象D,旧的Page可以在所有库存对象转移完成后回收,所有访问都可以通过指针和转发表转发到新的Page中。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_Shenandoah_09

  1. 并发重映射阶段将对所有引用进行修正,并删除转发记录。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_G1_10

  1. 下次并发标记开始后,由于上次垃圾回收循环尚未完成,因此Remapped指针标记为M1,用于区分上次的生存对象标记。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_JVM_11

可以看出,在并发标记的过程中,ZGC通过阅读屏障来保证访问的正确转发,而且由于染色指针采用惰性更新策略,每次访问转发指针的两次搜索速度比Shenandoah快得多。

染色指针的三大优点

1.由于染色指针提供的“自愈”能力,当一个Page被清除时,可以立即回收,而无需等待所有指向Page的修正。

2.ZGC根本不需要使用写屏障有两个原因:因为使用染色指针,不需要更新对象;没有分代,所以不需要记录跨代引用。

3.染色指针尚未完全开发和使用,其余18位提供了很大的可扩展性。

染色指针有一个自然的问题,即操作系统和处理器不完全支持程序对指针的修改。

各种内存映射

染色指针仅由JVM定义,操作系统和处理器可能不支持。ZGC正在解决这个问题Linux虚拟内存映射技术用于/x86-64平台。

ZGC为每个对象创建了三个虚拟内存地址RemappedMarked 0Marked 1,不同的染色标记用指针指向不同的虚拟内存地址。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_ZGC_12

分代

ZGC没有分代,这不是技术权衡,而是基于工作量。所以目前GC整体效率还有很大的提升空间。

读屏障

ZGC使用读取屏障来完成指针的“自愈”,ZGC没有写屏障,这成为ZGC的一大性能优势,因为ZGC目前还没有分代,ZGC通过扫描所有Region来节省卡表的使用。

NUMA

多核CPU同时操作内存,现代CPU将内存控制系统集成到处理器核心,每个CPU核心都有自己的本地内存。

ZGC现在在NUMA架构下有自己的本地内存分配对象,避免了内存使用的竞争。

在ZGC之前,只有Parallett, Scavenge支持NUMA内存分配。

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC | 京东云技术团队_Shenandoah_13

ZGC的操作步骤

ZGC和Shenadoah一样,几乎所有的运行阶段都与用户线程并发。它还包括STW的过程,如初始标记和重新标记,具有相同的功能,不再重复。重点介绍以下四个并发阶段:

并发标记

并发标记阶段与G1相同,通过遍历对象图进行可达性分析,不同的是ZGC标记在染色指针上。

并发准备重分配

在这个阶段,ZGC将扫描所有的Region。如果Region中的生存对象需要分配到新的Region中,则将这些Region放入重分配集中。

此外,JDK12后ZGC的卸载和弱引用处理也处于这个阶段。

并发重分配

在这个阶段,ZGC将重分配集中的Region中的库存对象复制到一个新的Region中,并对重分配集中的每个Region进行维护和转发,以记录从旧对象到新对象的映射关系。

在此阶段,如果用户线程并发访问重分配过程中的对象,并通过指针上的标记发现对象集中在重分配中,则读取屏障将被截获,通过转发内容转发访问,并修改参考值。

ZGC称这种行为为为自愈(Self-Healing),ZGC的设计导致只有在访问该指针时才会触发转发,这比每次转发Shenandoah的转发指针要好得多。

另一个好处是,如果Region中的所有对象都被复制,那么Region只要保留转发表,就可以回收。

并发重映射

最后一阶段的任务是纠正所有指针并释放转发表。

这一阶段的紧迫性并不高,因此ZGC将并发重映合并到下一个垃圾回收循环的并发标记阶段。无论如何,他们都需要遍历所有的对象。

总结

为了达到低停顿的目的,现代垃圾回收器可以说将“并发”一词发挥到了极致。Shenandoah在G1的基础上进行了大量的优化,使回收阶段并行进行。ZGC直接采用染色指针、NUMA等黑色技术,目的是让Java开发者更多地关注如何使用对象,使程序更好地运行,剩下的都交给GC,我们所做的就是享受现代GC技术带来的良好体验。

参考:

1.OpenJDK 17 中的 Shenandoah:亚毫秒级 GC 停顿【译】 - 知乎 (zhihu.com)

2.https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf

3.https://openjdk.java.net/jeps/333

上一篇 Nature研究成果遭抢发,两论文作者曾友好合作
下一篇 堆叠柱状图各成分连线画法:突出展示组间物种丰度变化

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题