一、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。