Jvm之ZGC垃圾收集器

一、什么是ZGC?

ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的实验性质的垃圾收集器。
对于Java项目而言,JVM进行垃圾回收总是绕不开一个问题,那就是Stop The World。对于很多大型项目或对响应时间有要求的项目,垃圾回收的停顿时间就是一个痛点,而ZGC就是一款低延迟的垃圾收集器。

二、ZGC的目标是什么?

ZGC要实现的目标

如图所示,ZGC的目标有四个:
(1)支持8MB~4TB级别的堆,JDK15后已经可以支持16TB
(2)垃圾收回的停顿时间最大不超过10ms(在JDK16中已经实现了不超过1ms)
(3)为将来的GC特性奠定基础
(4)最坏的情况下应用程序的吞吐量下降15%
另外官方提到ZGC的垃圾收集停顿时间不会随着堆大小增长而增长,也就是说GB级别和TB级别的收集时间都是一样。

三、ZGC内存布局

首先在ZGC中已经不存在分代的概念了,但是内存划分上和G1类似,都是划分为一个一个的小区域,在ZGC中称为页面(page)。
ZGC中页面的划分为三种:
小页面:小页面容量固定为2MB,用于存放小于等于256KB的对象。
中页面:中页面容量为32MB,用于存放大于等于256KB但小于4MB的对象。
大页面:大页面的容量不固定,可以动态变化,主要用于存放大于等于4MB的大对象。


ZGC内存布局.png

四、ZGC核心技术详解

4.1颜色指针

颜色指针.png

在ZGC以前,GC的信息都是保存在对象头的MarkWord中。而ZGC则剑走偏锋,直接将GC的信息存放在对象的指针上面,但是由于借用了指针的高几位,所以ZGC只能在64位上运行,而对应的指针压缩技术也就无法实现。
其中高18位是没有使用,都是为0,而剩下的46位中,只有42位是用来内存寻址的,另外4位则用来标记GC信息。
1位:Finalizable标识,它表示这个对象只能通过finalizer才能访问
1位:Remapped标识,是否进入了重分配集
其中2位:Marked0、Marked1是辅助GC标记的,用于判断是否是本次GC时标记的信息

4.2读屏障

读屏障类似Spring中Aop,对方法的一种增强手段,读屏障是JVM向应用代码插入一小段代码的技术。当用户线程从堆中读取对象的引用时,才会触发读屏障。

Object o = obj.fieldA; // 从堆加载对象引用
 //这里就需要读屏障
Object p = o; // 这里不是从堆中加载的,所以不需要读屏障
o.doSomething(); // 这里不是从堆中加载的,所以不需要读屏障
int i = obj.fieldB; // 这里不是对象的引用,所以也不需要读屏障

ZGC使用读屏障主要是进行两个操作,而且这两个操作是要一起做的原子操作,分别是:
1.对已经转移但还没有重定位的对象进行对象的重定位(这一步可以通过颜色指针的Marked0、Marked1区分)
2.删除对应对象再转发表中记录的指针新旧关系
触发时机:在两次GC之间业务线程访问这样的对象

五、ZGC的工作流程

ZGC工作流程大体上可以分为三个阶段:
1.标记阶段(标记存活对象)
2.对象转移阶段(转移存活对象)
3.对象重定位阶段(重定位对象指针)

ZGC执行流程.png

1.标记阶段(标记存活对象)
初始标记:这个阶段会停止用户线程的执行,然后从GC Roots出发,记录所有能直接引用的对象,这个阶段所耗时间只跟GC Roots能直接引用的对象数量有关,跟堆的大小无关。
并发标记:这个阶段不需要暂停用户线程,会扫描剩下存活的对象,执行时间会较长,并且可能会发生漏标问题。
再标记(最终标记):此阶段需要暂停用户线程,主要是处理上一步发生漏标的对象,使用读屏障+SATB算法解决。

2.对象转移阶段(转移存活对象)
并发转移准备:此阶段主要是分析页面垃圾对象的多少,无需暂停用户线程
初始转移:转移初始标记阶段标记的存活对象,并对转移的对象进行指针重定位,此阶段需要暂停用户线程
并发转移:此阶段不会暂停用户的线程,主要是转移剩下的存活对象,并且在对应的页面上维护一个转发表,用于记录对象转移后,指针的新旧关系。

3.对象重定位(并发重映射)
此阶段主要是对并发转移阶段,已转移但还没有重定位的对象,进行指针重定位。但是ZGC存在“指针自愈”,所以并不急着在本轮GC中将所有对象都重定位,而是放在下载GC的并发标记阶段,这样还可以节省一次遍历对象图的过程。一旦所有对象都重定位后,那么对应的转发表也会清空。
指针自愈:当对象转移后,对象的指针还没有重定位,此时如果用户线程访问了这个对象,就会从对应的转发表中找到最新的内存地址,然后再将指针重定位到新地址上,用户线程访问后就不会出错。
ZGC基于颜色指针的重定位算法
在下次GC中,对指针的标记和上次GC的标记会不同,技术上是使用Marked0和Marked1来区分。
这样就可以在下次GC的并发标记阶段,处理上次GC对象的指针重定位。

六、ZGC的触发时机

预热规则:JVM启动预热,如果从来都没有发生GC,那么在堆使用超过10%、20%、30%时,分别会触发一次GC。日志中关键字是“Warmup”。
基于分配速率的自适应算法:最主要的GC触发方式(也是默认方式),其算法原理是ZGC根据近期的对象分配速率以及GC时间,计算下次什么时候内存达到阈值,触发下次GC。通过ZAllocationSpikeTolerance参数控制阈值大小,默认值为2,数值越大,越早触发GC。日志中关键字是“Allocation Rate”。
基于固定时间间隔:通过ZCollectionInterval控制,会在指定的时间内进行垃圾回收。
主动触发规则:类似于固定间隔规则,但是间隔时间不确定,是ZGC自行计算得出,如果已经设置固定时间间隔触发,那么就需要通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。
阻塞内存分配请求触发:当垃圾来不及回收,堆空间将要占满时,会导致部分线程阻塞,在实际使用中,应当尽量避免此种情况发生。日志中关键字是“Allocation Stall”。
外部触发:通过代码显示调用System.gc()触发。但是由于G1和ZGC都是通过数据进行计算垃圾回收的信息,如果通过显示调用,ZGC和G1的部分功能可能不会执行。日志中关键字是“System.gc()”。
元数据分配触发:元数据区内存不足时触发,一般无需关注。日志中关键字是“Metadata GC Threshold”。

七、ZGC适用场景

目前ZGC不支持指针压缩和分代GC,其内存的占用会比G1的要大很多,因此ZGC更合适在大堆场景下去使用.
1.在超大堆上适用。在百GB或TB级堆应用上,ZGC的停顿时间仍然会保持在处理十几GB堆一样,而CMS和G1可能执行时间就是分钟级别。
2.在业务有硬性要求下,停顿时间必须低于100ms,那么此类场景下ZGC必然是首推的。

你可能感兴趣的:(Jvm之ZGC垃圾收集器)