ZGC(Z Garbage Collector)收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射,转发表 等技术来实现可并发的标记-整理算法 的,以低延迟为首要目标的一款垃圾收集器。
我们要知道的是 读屏障、染色指针和内存多重映射 是为了解决在并发-整理过程中 对象移动、引用更新这些问题的解决方案;所以,我们先介绍ZGC的内存布局,然后ZGC垃圾收集过程的几个阶段,随后再介绍上述几个技术来解决并发阶段产生的问题。
一、ZGC内存布局
与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region具有动态性------动态创建和销毁,以及动态的区域容量大小。
ZGC的Region可以具有大、中、小三类容量:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作"大型Region",但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。
二、ZGC垃圾收集阶段
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。
ZGC一个垃圾收集周期经历的几个阶段:
-
标记阶段:
先说明一下:在初始阶段,ZGC初始化之后,此时地址视图位Remapped,程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动。
0. 初始标记:从GC Roots出发标记全部直接子节点的过程;其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加该阶段是STW的。- 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。但是这个阶段会产生漏标问题(ZGC是利用原始快照
SATB
来解决,并且是利用「读屏障」+「多视图映射」来实现 SATB 的)。 - 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
- 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。但是这个阶段会产生漏标问题(ZGC是利用原始快照
-
转移阶段:(重分配阶段)
- 并发转移准备:准备两件事情,①、确定要清理的Region,将这些Region组成重分配集(Relocation Set);②、为每一个要清理的Region创建并维护一个转发表(Forward Table)。
- 初始转移:转移(初始标记阶段 的存活对象),同时做对象重定向,该阶段是STW的。
- 并发转移:转移(并发标记阶段的存活对象),该阶段是 用户线程和GC线程同时进行的。
三、图解ZGC完整周期
整个图解分为了两个ZGC周期,才能介绍完整个ZGC的功能和特点。
3.1 第一次ZGC
初始可达性分析图
标记阶段
1、初始标记
从GC Roots出发标记全部直接子节点的过程。
2、并发标记
从GC Roots开始对堆中对象进行可达性分析,找出存活对象。
3、再标记阶段
重新标记那些在并发标记阶段发生变化的对象。
转移阶段
1、并发转移准备阶段
2、初始转移阶段
转移(初始标记阶段的存活对象),并做对象重定向。
3、并发转移阶段
转移(并发标记阶段的存活对象)。
同时,我们可以看到这两个Region在存活对象被移走之后,立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。这就是染色指针的作用。
用户线程访问对象4
我们可以看到,当线程访问对象4的时候,发生在对象4指向对象5的引用,由旧地址指向新地址。
这得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的"自愈"(Self Healing)能力。
3.2 第二次ZGC
标记阶段
1、初始标记
和第一次ZGC一样。
2、并发标记
注意:绿色箭头和红色箭头变成了蓝色箭头,即我们所说的,这个阶段不仅会标记存活对象,同时,还会对上一次ZGC后,由于对象转移导致地址转变产生新地址,对这些对象进行地址重定向,使对象的引用由旧引用到新引用的转向关系。
3、再标记阶段
重新标记那些在并发标记阶段发生变化的对象。
转移阶段
1、并发转移准备阶段
这里当某一个Region中的所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
完事,这才是ZGC所涉及的完整过程!!!
剩下就是重复步骤。
四、ZGC涉及的技术
我这里把读屏障、染色指针、转发表和内存多重映射, 这些ZGC用到的技术放在最后讲,其主要原因是对于一些抽象的概念,如果上来放在开篇将的话很多人不知道怎么回事,也不知道为什么要用到;所以,我放在了最后解释。
-
读屏障:解决 GC线程和用户线程并发过程中,保证数据的准确性。像:用户线程 读取A对象内部有引用B对象的引用,而B对象正在被GC线程移动这样的读取问题,读屏障会给读操作添加额外的处理,保证数据的准确性。
-
转发表:解决 对象移动之后,让指向旧地址的引用更新为指向新地址。如:B对象移动到新Region中,当 A对象引用B对象时,发现B对象地址改变了,新地址和旧地址直线的映射关系如何找到呢,就是通过这转发表,使得对旧地址的引用指向新地址。
-
染色指针和内存多重映射:
染色指针解决ZGC垃圾收集过程中,存活对象移动到新Region时,对该存活对象的标记。内存多重映射:解决经过多重映射转换后,还可以用染色指针正常进行寻址。
4.1 读屏障
读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码(有点类似AOP技术)。需要注意的是,仅"从堆中读取对象引用"才会触发这段代码。
读屏障示例:
less
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i = obj.FieldB // 无需加入屏障,因为不是对象引用
ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
4.2 转发表(Forward Table)
在转移阶段,由于存活对象从一个A Region复制到另外一个B Region中,ZGC会为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系。
并且,得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的"自愈"(SelfHealing)能力。
解释一下这句话:ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中
因为染色指针就是再对应的引用地址上,利用了Remapped、Marked 0 、Marked 1标记移动对象的,所以可以通过染色指针知道,如果对象在重分配集之中,那么染色指针标记是:Marked 0 或 Marked 1,就可以利用转发表去访问新复制的对象上。
4.3 染色指针、内存多重映射
4.3.1 染色指针
首先,ZGC中的染色指针 类似G1中的卡表、Shenandoah中的转发指针,解决ZGC垃圾收集过程中,存活对象移动到新Region时,对该存活对象的标记。
这里我们需要看 第三章 ZGC完整周期
整个过程中 对象(方块),对应引用(箭头)的颜色。
- 深蓝色:ZGC初始状态
- 红色:Marked
- 绿色:Remapped + Relocated
- 浅蓝色:Remapped +Marked
ZGC过程中:
- 对象的初始染色指针是 Remapped (0100) :深蓝色;
- 标记阶段,被标记的对象染色指针是Marked 0(0001):红色;
- 转移阶段,被复制的对象染色指针重新转为Remapped(0100):绿色;
- 而当第二次ZGC时候,再进入标记阶段,存活的对象染色指针就是Marked 1(0010):浅蓝色
这里我们应该可以注意到,Remapped,Marked 0,Marked 1在ZGC过程中,对同一个对象来说,始终只能有个生效。
还有,这里引用 参考文章[1] 标记阶段 和 转移阶段 出现用户线程和GC线程并发访问对象时候,对染色指针的影响。
-
标记阶段: (对象的访问可能来自GC线程和应用程序线程)
下图就是在标记阶段,遇到:GC线程和应用线程访问 对象此时染色指针可能有的状态的情况。
在标记阶段结束之后,对象的地址视图要么是M0,要么是Remapped。 (其实就是 Remapped 标记为 Marked 0 的过程)
- 如果对象的地址视图是M0,说明对象是活跃的;
- 如果对象的地址视图是Remapped,说明对象是不活跃的,即对象所使用的内存可以被回收。
-
转移阶段: (对象的访问可能来自GC线程和应用程序线程)
Marked 0 重新标记成 Remapped 的过程。
这个阶段,当一个Region中所有存活对象被移走之后,立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。这就是染色指针的作用。
-
然后第二次GC的标记阶段:
再进入标记阶段,存活的对象染色指针就是Marked 1,不活跃的对象还是是为Marked 0。然后重复上面转移阶段的就完事。
4.3.2 内存多重映射
内存多重映射其实是染色指针技术的一个伴生产物,并不是专门为了实现其他某种特性需求而去做的。 因为我们Java程序其实就是JVM启动的一个进程罢了,进程里面使用的内存地址其实是虚拟地址(这个需要看看计算机组成原理 ^ v ^),对应真实的物理地址是由操作系统利用虚拟内存映射技术实现的;这里的内存多重映射其实就:让同一个对象因为移动产生的不同的虚拟地址映射指向同一个物理地址上,即虚拟地址和物理地址多对一的映射关系。
进而,染色指针的几个标志其实可以看做是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。
染色指针在Linux/x86_64架构下,地址映射的二进制表现形式如下图所示:
参考文章[1]:cloud.tencent.com/developer/a...
参考文章[2]:tech.meituan.com/2020/08/06/...
参考文章[3]:www.lihuibin.top/archives/a8...
参考文章[4]:blog.csdn.net/zhangzhigan...
参考文章[5]:cr.openjdk.java.net/~pliden/sli...
参考书籍[6]:周志明-深入理解Java虚拟机