关于jvm:从原理聊JVM三详解现代垃圾回收器Shenandoah和ZGC-京东云技术团队

3次阅读

共计 6054 个字符,预计需要花费 16 分钟才能阅读完成。

作者:京东科技 康志兴

Shenandoah

Shenandoah 一词来自于印第安语,十九世纪四十年代有一首驰名的航海歌曲在水手中广为流传,讲述一位年老富商爱上印第安酋长 Shenandoah 的女儿的故事。起初美国有一条位于 Virginia 州西部的小河以此命名,所以 Shenandoah 的中文译名为“情人渡”。

Shenandoah 首次呈现在 Open 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(二):从串行收集器到分区收集开创者 G1

连贯矩阵(Connection Matrix)

G1 中每个 Region 都要保护卡表,既消耗计算资源还占据了十分大的内存空间,Shenandoah 应用了连贯矩阵来优化了这个问题。

连贯矩阵能够简略了解为一个二维表格,如果 Region A 中有对象指向 Region B 中的对象,那么就在表格的第 A 行第 B 列打上标记。

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

相比 G1 的记忆集来说,连贯矩阵的颗粒度更粗,间接指向了整个 Region,所以扫描范畴更大。但因为此时 GC 是并发进行的,所以这是通过抉择更低资源耗费的连贯矩阵而对吞吐进行斗争的一项决策。

转发指针

转发指针的性能劣势

想要达到并发回收,就须要在用户线程运行的同时,将存活对象逐渐复制到空的 Region 中,这个过程中就会在堆中同时存在新旧两个对象。那么如何让用户线程拜访到新对象呢?

此前,通常是在旧对象原有内存上设置爱护陷阱(Memory Protection Trap),当拜访到这个旧对象时就会产生自陷异样,使程序进入到预设的异样处理器中,再由处理器中的代码将拜访转发到复制后的新对象上。

自陷是由线程发起来打断以后执行的程序,进而取得 CPU 的使用权。这一操作通常须要操作系统参加,那么就会产生用户态到内核态的转换,代价非常微小。

所以 Rodney A.Brooks 提出了应用转发指针来实现通过旧对象拜访新对象的形式:在对象头后面减少一个新的援用字段,在非并发挪动状况下指向本人,产生新对象后指向新对象。那么当拜访对象的时候,都须要先拜访转发指针看看其指向哪里。尽管和内存自陷计划相比同样须要多一次拜访转发的开销,然而前者耗费小了很多。

转发指针的问题

转发指针次要存在两个问题:批改时的 线程平安问题 和高频拜访的 性能问题

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 被称为 Immediate Garbage Region)。

  1. 并发回收(Concurrent Evacuation)

将回收集外面的存货对象复制到一个其余未被应用的 Region 中。并发复制存活对象,就会在同一时间内,同一对象在堆中存在两份,那么就存在该对象的读写一致性问题。Shenandoah 通过应用转发指针将旧对象的申请指向新对象解决了这个问题。这也是 Shenandoah 和其余 GC 最大的不同。

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

并发回收后,须要将所有指向旧对象的援用修改到新对象上。这个阶段实际上并没有实际操作,只是设置一个阻塞点来保障上述并发操作均已实现。

  1. 并发援用更新(Concurrent Update References)

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

  1. 最终援用更新(Final Update References)[STW]

堆空间中的援用更新结束后,最初须要修改 GC Roots 中的援用。

  1. 并发清理(Concurrent Cleanup)

此时回收集中 Region 应该全副变成 Immediate Garbage Region 了,再次执行并发清理,将这些 Region 全副回收。

ZGC

ZGC 是 Oracle 官网研发并 JDK11 中引入,并于 JDK15 中作为生产就绪应用,其设计之初定义了三大指标:

1. 反对 TB 级内存

2. 进展管制在 10ms 以内,且不随堆大小减少而减少

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

随着 JDK 的迭代,目前 JDK16 及以上版本,ZGC 曾经能够实现不超过 1 毫秒的进展,实用于堆大小在 8MB 到 16TB 之间。

ZGC 的内存布局

ZGC 和 G1 一样也采纳了分区域的堆内存布局,不同的是,ZGC 的 Region(官网称为 Page,概念同 G1 的 Region)能够动态创建和销毁,容量也能够动静调整。

ZGC 的 Region 分为三种:

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

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

3. 大型 Region 容量为 2MB 的整数倍,寄存 4MB 及以上大小的对象,而且每个大型 Region 中只寄存一个大对象。因为大对象挪动代价过大,所以该对象不会被重调配。

重调配集(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 在指针中减少了 4 个标记位,包含 FinalizableRemappedMarked 0Marked 1

源码正文如下:

 6                 4 4 4  4 4                                             0
 3                 7 6 5  2 1                                             0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
|                   | |    |
|                   | |    * 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 过程中的作用

咱们通过红蓝黄三个色彩别离示意三种标记状态:

1. 第一次标记开始时所有的指针都处于 Remapped 状态

  1. 从 GC Root 开始,顺着对象图遍历扫描,存活对象标记为M0
  1. 标记实现后,开始进行并发重调配。最终目标是将 A、B、C 三个存活对象都挪动到新的 Region 中去。

整个标记过程中新调配到对象都被间接标记为 M0,比方对象 D。

复制实现的对象,指针就能够由 M0 改为 Remapped,并将旧对象到新对象到映射关系保留到转发表中。

  1. 如果此时零碎拜访对象 C,会触发读屏障,将原援用修改到新的对象 C 的地址下来,并转发拜访,最初删除转发表的记录。

这个行为称为指针的“自愈”。

实际上,如果没有对象 D 的存在,在上一步所有存货对象转移实现后,旧的 Page 就能够被回收了,依附指针和转发表就能够将所有拜访转发到新的 Page 中去。

  1. 并发重映射阶段会把所有援用修改,并删除转发表的记录。
  1. 下一次并发标记开始后,因为上一次垃圾回收循环并没有实现,所以 Remapped 指针被标记为M1,用来和上一次的存活对象标记作辨别。

能够看出,并发标记的过程中,ZGC 是通过读屏障来保障拜访的正确转发,并且因为染色指针采纳惰性更新的策略,相比 Shenandoah 每次都要先拜访转发指针的两次寻址来说快上不少。

染色指针的三大长处

1. 因为染色指针提供的“自愈”能力,当某个 Page 被革除后能够立即被回收,而无需期待修改全副指向该 Page 的援用。

2.ZGC 齐全不须要应用写屏障,起因有二:因为应用染色指针,无需更新对象体;没有分代所以无需记录跨代援用。

3. 染色指针并未齐全开发应用,剩下的 18 位提供了十分大的扩展性。

而染色指针有一个人造的问题,就是操作系统和处理器并不齐全反对程序对指针的批改。

多种内存映射

染色指针只是 JVM 定义的,操作系统、处理器未必反对。为了解决这个问题,ZGC 在 Linux/x86-64 平台上采纳了虚拟内存映射技术。

ZGC 为每个对象都创立了三个虚拟内存地址,别离对应 RemappedMarked 0Marked 1,通过指针指向不同的虚拟内存地址来示意不同的染色标记。

分代

ZGC 没有分代,这一点并不是技术衡量,而是基于工作量的思考。所以目前来看,整体的 GC 效率还有很大晋升空间。

读屏障

ZGC 应用了读屏障来实现指针的“自愈”,因为 ZGC 目前没有分代,且 ZGC 通过扫描所有 Region 来省去卡表应用,所以 ZGC 并没有写屏障,这成为 ZGC 一大性能劣势。

NUMA

多核 CPU 同时操作内存就会产生争抢,古代 CPU 把内存控制系统器集成到处理器内核中,每个 CPU 外围都有属于本人的本地内存。

在 NUMA 架构下,ZGC 会有当初本人的本地内存上调配对象,防止了内存应用的竞争。

在 ZGC 之前,只有 Parallet Scavenge 反对 NUMA 内存调配。

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

正文完
 0