外观
ZGC垃圾回收器
约 1587 字大约 5 分钟
2025-11-08
为什么要学习ZGC
ZGC(Z Garbage Collector)是JDK 11引入的一款超低延迟垃圾收集器,目标是将GC暂停时间控制在10毫秒以内,即使在TB级大堆内存下也能保持极低的延迟。ZGC采用了并发、分区、染色指针等多项前沿技术,是现代Java应用追求极致响应速度的首选。 
对比G1
G1垃圾收集器采用了部分区域回收的处理方式,解决了传统垃圾收集器中全堆扫描所带来的性能问题,缩短堆内存较大情况下的停顿时间。尽管G1经过多个版本的优化和调优,已经接近极限,但仍然无法满足日益增长的机器内存需求。
根本问题,还是G1的性能不能满足现阶段的硬件配置,G1的GC停顿时间相对较长,G1的最大停顿时间达到了610ms。
核心特性
堆内存布局
布局和G1类似,采用了分区模型,把堆内存分成等大小的Region。从jdk21开始支持分代模型。 
内存指针存储GC信息
传统垃圾回收器通过扫描堆中的对象(扫描堆空间是很慢的),根据对象头中的可达性标记信息,来确定对象是否应该被回收。
ZGC不直接依赖于对象头中的信息来进行垃圾回收决策,而是把GC信息存在内存引用地址上。GC时通过扫描栈上的内存引用指针来确定对象的引用关系和可达性,从而来判断对象是否应该被回收。 
Z通过64位指针(64位操作系统才支持)的高位来标识对象的可达性,其中第44位到47位标识GC信息。 
查看源代码 
GC标记过程
简单点概括,就是先标记存活对象,再把存活动向转移到空闲内存分区,最后回收。 
- 初始标记
扫描所有线程栈的根节点,然后再扫描根节点直接引用的对象并进行标记。这个阶段需要停顿所有的应用线程(STW),但由于只扫描根对象直接引用的对象,所以停顿时间很短。停顿时间高度依赖根节点的数量,从JDK16开始,已经解决了此问题:https://malloc.se/blog/zgc-jdk16
- 并发标记/并发对象重定位
第1个GC周期:并发遍历上一次标记下引用的对象并标记。
第2个GC周期:并发遍历的过程中,顺便把上周期"并发迁移"阶段迁移的对象指针修正指向到新分区。
- 标记结束/再标记
标记上一次标记过程新产生的对象。并发标记过程中,应用线程可能会产生一些新对象,所以需要再标记出来。这个阶段需要停顿所有的应用线程。(STW),但由于只标记新增的对象,数量很少,所以停顿时间很短。
- 并发转移准备
为对象转移做一些前置准备,比如引用处理、弱引用清理和重定位集选择等。
5、转移开始/初始转移
迁移根节点直接引用的对象到新分区,这个阶段需要停顿所有的应用线程(STW),但由于只迁移根节点直接引用的对象,所以停顿时间很短。
6、并发迁移
并发迁移“并发标记”阶段标记的对象到新分区(对象引用指针未修改,仍指向旧分区)。
为何并发转移阶段,对象已转移至新分区后,却没有修改线程栈上实际的引用,依然指向旧分区?
因为如果此时再扫描线程栈,修改引用地址,要扫描的量太大,效率太低。
刚好下一个GC周期也要进行扫描标记,可以利用扫描标记的时间,同时把对象引用修正指向到新分区,以此提升效率,减少停顿时间。
并发转移阶段对象已迁移,但引用指针仍指向旧分区,如何保证旧分区被清理后对象仍然可以访问?
由于未修改对象引用指针,为防止旧分区被清理,导致对象找不到的问题,此处引入了读屏障和转发表,转发表记录了对象从旧位置到新位置的映射关系,实现类似一个hash表,key是旧分区的位置,value是新分区的位置,此时当访问旧位置的对象时,通过转发表可以获取新位置。这样可以避免在整个堆空间中更新对象引用的开销,因为只需要更新转发表中的条目即可。
读屏障的作用是在读取对象引用时,检查对象的标记状态并获取转发表中的映射关系。通过读屏障,ZGC能够在读取对象引用时,将访问重定向到新位置,以确保对象的访问仍然有效。如下图:每次读取引用时会触发一次读屏障
GC日志分析
把上面的关键日志贴到到GC示意图中来分析实际的GC过程,可以发现总停顿时间只有0.07ms,符合官方说的小于1ms 
GC调优
ZGC 被设计为自适应且需要最少的手动配置。在 Java 程序执行期间,ZGC 通过调整代大小、扩展 GC 线程数量以及调整保有阈值来动态适应工作负载。主要的调整旋钮是增加最大堆大小。 不再需要调整–Xmn、–XX:TenuringThreshold 和–XX:ConcGCThreads(动态调整,JDK17开始)等 只需要设置:–Xmx -XX:+UseZGC