一、ZGC 介绍
ZGC(Z Garbage Collector)是一种可扩展的低延迟垃圾收集器,旨在满足以下目标:
- 亚毫秒最大停顿时间[1](Sub-millisecond max pause times);
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加(Pause times do not increase with the heap, live-set or root-set size);
- 支持8MB到16TB的堆(Handle heaps ranging from a 8MB to 16TB in size)。
ZGC最初在JDK11中作为一项实验性功能引入,JDK13将支持的最大堆从4T增加到16T,JDK14将最小堆支持到8M,在JDK15中宣布可用于生产,JDK16发布之后实现了最大停顿时间不超过1ms,使用参数–XX:+UseZGC
就可以开启ZGC。
官网关于ZGC的关键词:并发(Concurrent)、基于区域(Region-based)、压缩(Compacting)、NUMA[2]感知(NUMA-aware)、使用染色指针[3](Using colored pointers)、使用读屏障(Using load barriers)。
二、GC过程
ZGC依旧采用标记-复制算法,在标记、转移、重定位阶段几乎都是并发进行的,这也是ZGC可以实现亚毫秒级停顿的关键原因。只有三个STW阶段:初始标记、再标记、初始转移。
- 初始标记(Phase 1: Pause Mark Start)
这个阶段需要STW,切换到marked视图,为并发标记做准备。JDK16之前会在这个阶段标记GC ROOT,之后改为并发标记GC ROOT。- 并发标记(Phase 2: Concurrent Mark)
遍历整个堆中存活的对象,并将其指针染色。顺便还会修复坏指针。- 完成初始标记(Phase 3: Pause Mark End)
这个阶段会STW,判断标记是否完成。- 并发标记释放(Phase 4: Concurrent Mark Free)
释放所有未使用的标记堆栈空间。- 并发处理软引用、弱引用(Phase 5: Concurrent Process Non-Strong References)
- 并发重置转移集(Phase 6: Concurrent Reset Relocation Set)
重置Relocation Set。- 验证(Phase 7: Pause Verify)
验证GC状态。- 并发选择转移集(Phase 8: Concurrent Select Relocation Set)
一次GC中可能会有很多分区可以被回收,在这个阶段会选择回收价值较高的分区,把他们放入Relocation Set。- 初始转移(Phase 9: Pause Relocate Start)
切换到remapped视图,为并发转移做准备。- 并发转移(Phase 10: Concurrent Relocate)
遍历Relocation Set,将存活的对象迁移。
三、关键技术
3.1 内存多重映射
使用mmap将不同的虚拟内存地址映射到同一物理地址上。如下图:
当应用创建对象时,会在堆上申请一个虚拟地址,ZGC会为这个对象在Marked0、Marked1和Remapped三个视图上分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址,这三个视图在同一时间只有一个是有效的,ZGC就是通过切换这三个视图实现并发的垃圾回收。
3.2 染色指针
之前的垃圾收集器是将GC信息存在对象头的Mark Word中(在64位虚拟机中0~1位锁标志,2~63位GC标记及分代年龄),ZGC将GC信息放在对象指针中,0~43位为对象地址,44~47位为标志位,其余16位为0,因此ZGC最大可以管理16TB(244)内存。通过这四个标志位,JVM 不用访问对象就可以直接从指针上分辨出对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。
3.3 内存布局
G1将整个堆内存分成了大小相同的Region,每个Region的大小可以通过-XX:G1HeapRegionSize
来设置,大小为1~32MB(必须是2n),默认有2048个Region,因此,G1能管理的最大堆内存为64GB(2048*32MB),最小堆内存为2GB(2048*1MB)。
ZGC的堆与G1类似,也是基于Region分布的,不同的地方在于ZGC不分代、动态创建和销毁以及大小不固定,包括三种类型的Region:
- Small Region:2MB,主要用于放置小于256KB的小对象。
- Medium Region:32MB,主要用于放置大于等于256KB小于4MB的对象。
- Large Region:N*2MB,这个类型的Region是可以动态变化的,不过必须是2MB的整数倍,最小支持4MB。每个Large Region只放置一个大对象,并且是不会被重分配的。
3.4 读屏障(Load Barrier)
读屏障类似Spring AOP的前置增强,JVM在应用代码中插入一小段代码,当线程从堆中读取对象的引用时,就会执行这段代码。
1 | Object o = obj.FieldA // 从堆中读取引用,需要加入屏障 |
读屏障的作用:由于GC线程和应用线程是并行执行的,就会存在某一时刻对象A中引用对象B,此时对象B已被转移,也就是两个对象处在不同的视图中,当应用线程去读取对象B时,就会发现对象B已被转移,就可以修正对象的引用,获取到的也是最新的引用。
3.5 堆栈水印屏障(Stack Watermark Barrier)
众所周知,STW发生在安全点(safe-point),之前的垃圾收集器会在STW期间标记GC Root,这个过程需要扫描线程堆栈,如果应用拥有大量线程,那么STW的时间就会增加,如果这些线程的调用栈很深的话,这个时间会更长。从JDK16开始,扫描线程堆栈变成了并发进行。
在应用线程运行的同时,去扫描线程堆栈,就需要用到一个叫做堆栈水印屏障的技术。这是一种可以防止线程在没有检查是否安全的情况下返回栈帧的机制。具体细节可以查看 JEP 376: ZGC: Concurrent Thread-Stack Processing。