作者:京东科技 康志兴
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的运转进程
- 初始符号(Init Mark)[STW] [同G1]
符号与GC Roots直接相关的方针。
- 并发符号(Concurrent Marking)[同G1]
遍历方针图,符号悉数可达方针。
- 终究符号(Final Mark)[STW] [同G1]
处理剩余的SATB扫描,并在这个阶段统计出收回价值最高的Region,将这些Region构成一组收回集。
- 并发整理(Concurrent Cleanup)
收回一切不包括任何存活方针的Region(这类Region被称为Immediate Garbage Region)。
- 并发收回(Concurrent Evacuation)
将收回集里边的存货方针仿制到一个其他未被运用的Region中。并发仿制存活方针,就会在同一时间内,同一方针在堆中存在两份,那么就存在该方针的读写共同性问题。Shenandoah经过运用转发指针将旧方针的恳求指向新方针处理了这个问题。这也是Shenandoah和其他GC最大的不同。
- 初始引证更新(Init Update References)[STW]
并发收回后,需求将一切指向旧方针的引证批改到新方针上。这个阶段实际上并没有实际操作,只是设置一个阻塞点来确保上述并发操作均已完结。
- 并发引证更新(Concurrent Update References)
顺着内存物理地址线性遍历堆空间,更新并发收回阶段仿制的方针的引证。
- 终究引证更新(Final Update References)[STW]
堆空间中的引证更新结束后,最后需求批改GC Roots中的引证。
- 并发整理(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个标志位,包括Finalizable
、Remapped
、Marked 0
和Marked 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()
办法拜访到,Remapped
、Marked 0
和Marked 1
用作三色符号(后面简称为M0
和M1
)。
为什么既有M0
还有M1
呢?
因为ZGC符号完结后并不需求等候方针指针重映射就能够进行下一次废物收回循环,也便是说两次废物收回的全进程是有堆叠的,所以运用两个符号位别离用作两次相邻GC进程的符号,M0
和M1
替换运用。
染色指针的在GC进程中的效果
咱们经过红蓝黄三个色彩别离表明三种符号状况:
1.第一次符号开端时一切的指针都处于Remapped
状况
- 从GC Root开端,顺着方针图遍历扫描,存活方针符号为
M0
- 符号完结后,开端进行并发重分配。终究方针是将A、B、C三个存活方针都移动到新的Region中去。
整个符号进程中新分配到方针都被直接符号为M0,比方方针D。
仿制完结的方针,指针就能够由M0改为Remapped,并将旧方针到新方针到映射关系保存到转宣布中。
- 假如此刻体系拜访方针C,会触发读屏障,将原引证批改到新的方针C的地址上去,并转发拜访,最后删去转宣布的记载。
这个行为称为指针的“自愈”。
实际上,假如没有方针D的存在,在上一步一切存货方针转移完结后,旧的Page就能够被收回了,依靠指针和转宣布就能够将一切拜访转发到新的Page中去。
- 并发重映射阶段会把一切引证批改,并删去转宣布的记载。
- 下一次并发符号开端后,因为上一次废物收回循环并没有完结,所以
Remapped
指针被符号为M1
,用来和上一次的存活方针符号作区别。
能够看出,并发符号的进程中,ZGC是经过读屏障来确保拜访的正确转发,而且因为染色指针选用慵懒更新的战略,比较Shenandoah每次都要先拜访转发指针的两次寻址来说快上不少。
染色指针的三大长处
1.因为染色指针提供的“自愈”才能,当某个Page被清除后能够立刻被收回,而无需等候批改悉数指向该Page的引证。
2.ZGC彻底不需求运用写屏障,原因有二:因为运用染色指针,无需更新方针体;没有分代所以无需记载跨代引证。
3.染色指针并未彻底开发运用,剩余的18位提供了十分大的扩展性。
而染色指针有一个天然的问题,便是操作体系和处理器并不彻底支撑程序对指针的批改。
多种内存映射
染色指针只是JVM界说的,操作体系、处理器未必支撑。为了处理这个问题,ZGC在Linux/x86-64平台上选用了虚拟内存映射技能。
ZGC为每个方针都创建了三个虚拟内存地址,别离对应Remapped
、Marked 0
和Marked 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.shipilev.net/talks/devox…
3.openjdk.java.net/jeps/333
系列文章:
从原理聊JVM(一):染色符号和废物收回算法
从原理聊JVM(二):从串行搜集器到分区搜集开创者G1