Skip to content

ZGC 原理与彩色指针

提出问题

GC 停顿一直是 Java 应用的心病。Parallel GC 的 Full GC 停顿按秒算,G1 能把 200ms 以内的停顿做的比较稳,但一旦堆大小超过 64GB,G1 的 Mixed GC 停顿时长还是会随堆增长而增长。CMS 虽然并发标记,但内存碎片、Concurrent Mode Failure 问题让它被 JDK 14 正式移除。

面试官问 ZGC,核心是想确认:ZGC 如何在 10ms 以内完成 GC 且与堆大小无关?"彩色指针"和"读屏障"到底是干什么的,和 G1 的做法有什么本质区别?如果面试者能答出"在指针上做文章而不是在对象头上做文章",说明对 GC 架构有深层理解。

分析问题

传统 GC 的 STW 瓶颈在哪

先看传统 GC 为什么停得久。不管是 G1 还是 CMS,STW 最长的阶段通常是重定位(Relocation)——把存活对象从 A 区域复制到 B 区域,然后更新所有指向 A 的引用。引用更新必须 STW,因为复制过程中用户线程可能正在访问 A 区域的对象,如果不 STW,读到的可能是不完整的对象。

G1 的解决思路是:只复制一部分 Region,但复制期间仍然 STW。它通过停顿预测模型把每次 STW 控制在目标时间内(默认 200ms),但堆越大,要处理的 Region 越多,停顿就越难压下来。

ZGC 的解法完全不同:把重定位做成并发的,不需要 STW。它靠的是两个核心机制——彩色指针和读屏障。

彩色指针:把元数据塞进指针里

ZGC 利用 64 位指针的高 4 位(第 42-45 位)存储对象的 GC 元数据。这 4 位分别标记:

  • Finalizable:对象是否可被终结器引用
  • Remapped:对象是否已被重定位到新地址
  • Marked1 / Marked0:标记位,交替使用

一个 64 位指针的布局大致如下:

63  62  61  60  59  58  57  56  55 ... 48  47  46  45  44  43  42  41 ... 0
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|   unused (18 bit)    | F | R | M1| M0|       42-bit 地址空间          |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

4 位颜色位意味着 ZGC 只能在 64 位系统上运行,且需要 4TB 以上的虚拟地址空间来映射堆(42 位寻址空间 = 4TB)。这也是 ZGC 不支持压缩指针(-XX:+UseCompressedOops)的原因——压缩指针已经占用了高几位,彩色指针就没位置了。

彩色指针的好处是巨大的:GC 标记信息不需要写在对象头里,所以读对象头不需要同步。传统的 Mark Word 在 GC 标记时需要更新对象头,涉及 CAS 和内存屏障,而 ZGC 直接在指针层面判断对象状态,读指针时颜色位就已经告诉你了。

读屏障与自愈

ZGC 的读屏障(Load Barrier)是一段轻量级代码,在每次从堆中读取引用时执行。它不是硬件屏障,是编译器在 JIT 编译时插入的检查逻辑。

读屏障的伪代码逻辑:

java
// 伪代码:ZGC 读屏障逻辑
Object loadReference(Object ref) {
    if (ref == null) return null;
    
    // 检查指针的颜色位,判断对象状态
    if (isMarked0(ref) || isMarked1(ref)) {
        // 对象在 GC 标记阶段已被标记,正常返回
        return ref;
    }
    
    // 对象可能处于已重定位状态
    // 通过转发表查找新地址
    Object newRef = forwardingTable.get(ref);
    if (newRef != null) {
        // 自愈:把当前引用更新为新地址,下次访问不再走转发表
        selfHeal(ref, newRef);
        return newRef;
    }
    
    // 此对象尚未被 GC 处理,直接返回
    return ref;
}

关键点是自愈(Self-Healing):如果读屏障发现指针指向的对象已经被移动了,它会把当前引用更新为新地址。下次同一个线程或其他线程通过同一个引用访问对象时,就直接命中新地址,不需要再查转发表。自愈的效果是"访问一次即修复",后续访问零开销。

这和 G1 的 RSet 维护思路完全不同——G1 是"GC 时帮你把所有引用更新好,但必须 STW",ZGC 是"不急着更新,让用户线程在访问时自己修复"。后者把 STW 的开销分散到了用户线程的执行路径上。

ZGC 的完整 GC 阶段

ZGC 的 GC 周期只有三个并发阶段:

  1. 并发标记(Concurrent Mark):从 GC Roots 出发,遍历对象图,在指针的 Marked0/Marked1 位上标记存活对象。这个过程和用户线程完全并发。
  2. 并发预备重定位(Concurrent Preparation for Relocation):扫描所有 Region,标记哪些 Region 需要重定位(垃圾最多的 Region 优先),分配转发表。
  3. 并发重定位(Concurrent Relocation):把存活对象从选中的 Region 复制到新 Region,同时在转发表中记录映射关系。用户线程访问旧地址时,通过读屏障自动重定向到新地址并自愈。

只有初始标记(Initial Mark)和最终标记(Final Mark)两个阶段需要 STW,但每次 STW < 1ms。这两个 STW 阶段只需要处理 GC Roots 本身,不需要遍历对象图。

总结

ZGC 的核心思想是用读屏障的运行时开销换取 STW 时间的极致压缩。彩色指针把 GC 元数据从对象头移到指针中,让标记状态和重定位状态可以在指针加载时直接判断;自愈机制把重定位后的引用更新延迟到用户线程访问时完成,避免了 STW 的全局引用更新。

特性G1ZGC
停顿时间50-200ms(随堆增长)<10ms(与堆大小无关)
重定位STW 阶段并发阶段 + 自愈
标记信息存储对象头 + 位图彩色指针(4 位)
第三方引用跟踪RSet(堆的 5-20% 内存)转发表 + 读屏障
压缩指针支持支持不支持
最小堆建议4GB+4GB+(需要 4TB 虚拟地址空间)

面试话术示例:"ZGC 的彩色指针在指针上编码了对象状态,省去了对象头上的 GC 标记字段,读屏障在访问时自动修复已移动的引用,所以重定位阶段不需要 STW。代价是只能在 64 位系统上跑,且每次读引用都要多一次颜色判断,但对延迟敏感的应用来说,这比 STW 的代价小得多。"

参考:OpenJDK ZGC 主页 https://wiki.openjdk.org/display/zgc/Main · 《深入理解 Java 虚拟机》周志明(第 3 版)· JDK 源码 src/hotspot/share/gc/z/

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。