作者:京东科技 康志兴

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