JVM 收集器之 ZGC
1、前言
上篇我们说了 G1,G1 有很多创建,比如分 region 收集,并发标记等,G1 运行的流程如图所示:
G1 运行流程
从上图可知,G1 在虽然在初始标记、最终标记是 STW 的,但是因为时间很短,所以忽略不计;可是在筛选回收的时候也是 STW,而复制标记后存活的对象耗时较长,所以 G1 主要的耗时在这里。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。而 ZGC 实现了并发回收,所以停顿时间很短。
2、概念
ZGC 也采用基于 Region 的堆内存布局,但与 G1 不同的是, ZGC 的 Region 具有动态性,也就是可以动态创建和销毁,容量大小也是动态的,有大、中、小三类容量:
ZGC 的 region
- 小型 Region (Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
- 中型 Region (M edium Region):容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对 象。
- 大型 Region (Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB。
ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。下面介绍着色指针和读屏障技术细节。
着色指针:着色指针是一种将信息存储在指针中的技术。
着色指针
- Marked0 /Marked1:标记位,标记对象是否可用,
- Remapped:记录对象是否进入过重分配集(对象是否移动过)
- Finalizable:标记对象是否只能通过fnalize()访问
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。
读屏障:读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。
3、流程
接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程:
- 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为 Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
- 并发标记阶段:第一次进入标记阶段时视图为 M0,如果对象被 GC 标记线程或者应用线程访问过,那么就将对象的地址视图从 Remapped 调整为 M0。所以,在标记阶段结束之后,对象的地址要么是 M0 视图,要么是 Remapped。如果对象的地址是 M0 视图,那么说明对象是活跃的;如果对象的地址是 Remapped 视图,说明对象是不活跃的。
- 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为 Remapped。如果对象被GC 转移线程或者应用线程访问过,那么就将对象的地址视图从 M0 调整为 Remapped。
其实,在标记阶段存在两个地址视图 M0 和 M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为 M1,而非 M0。
流程
简单的说就是 M1 标识本次垃圾回收中活跃的对象,而 M0 是上一次回收被标记的对象,但是没有被转移,在本次回收中也没有被标记活跃的对象。其实从上面的分析以及得知,如果没有被转移就会停留在 M0 这个地址视图。而下一次 GC 如果还是用 M0 来标识那混淆了这两种对象。
综上所述:染色指针是判断对象是否活跃,然后再判断是否需要转移的标志。而读屏障是为了在并发转移的过程中,应用线程读取到转移后对象,可以提前根据转移表更正染色指针指向的地址,让应用线程读取到新对象。
4、结论
并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。
5、参考资料
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html