Skip to content

09.1 - ZGC 原理

定位: 亚毫秒级停顿的全并发 GC——彩色指针 + 负载屏障,让 GC 不再是延迟瓶颈 面试高频度: ⭐⭐⭐⭐

一、这是什么?为什么需要它?

**垃圾收集器详解(Serial到ZGC)** 介绍了 GC 收集器的演进。在 ZGC 之前,已有收集器的 STW 停顿存在一个硬约束

  • CMS: STW 停顿随堆大小增长。32GB 堆 → CMS 并发标记阶段约 50-100ms
  • G1: 可预测停顿目标,但大堆(>64GB)下 STW 仍可能到 10-50ms

核心矛盾:现代应用需要低延迟(<10ms P99),传统 GC 做不到

ZGC(JDK 11 实验 → JDK 15 正式 → JDK 21 分代)的目标:无论堆多大(8MB 到 16TB),STW 停顿 < 1ms(实测通常在 0.2-0.5ms)。

二、原理拆解

2.1 为什么传统 GC 停顿长?

垃圾收集需要做三件事:标记(Mark)→ 重定位(Relocate)→ 重映射(Remap)。前两件事最难并发化:

  • 标记阶段要遍历整个对象图 → 如果应用线程同时修改引用(三色标记的"黑→白"问题),必须用 STW 或复杂屏障解决
  • 重定位要移动对象 → 移动一个对象后,所有引用它的指针都要更新。如果应用线程正在用旧地址访问,就会崩溃

G1 的重定位是 STW 的——暂停所有线程移动对象 + 更新引用。堆越大、对象越多,停顿越长。

2.2 ZGC 的核心突破

ZGC 用两项核心技术实现了全并发:

彩色指针(Colored Pointers)

传统 64 位指针:
┌──────────────────────────────────────────────────────────────────────────┐
│  0000 0000 0000 0000 00┌────────────────── 地址位 (48bit) ──────────────┤
└──────────────────────────────────────────────────────────────────────────┘

ZGC 彩色指针 (64bit):
┌──────┬──────┬──────┬──────┬──────────────────────────────────────────────┐
│      │      │      │      │                                               │
│  M1  │  M0  │ Rmtd │ Fin  │        地址位 (42bit → 4TB 可寻址)            │
│(1bit)│(1bit)│(1bit)│(1bit)│                                               │
└──────┴──────┴──────┴──────┴──────────────────────────────────────────────┘

M1   = Marked 1 (奇数 GC 周期已标记)
M0   = Marked 0 (偶数 GC 周期已标记)
Rmtd = Remapped (指针已更新为新地址)
Fin  = Finalizable (只被 Finalizer 可达)

为什么把这个信息放进指针?

传统 GC 需要额外 metadata(如 G1 的 RSet、记忆集)来记录对象状态。ZGC 把"对象当前处于哪个 GC 阶段"编码到指针本身中——指针就是 metadata

这意味着:

  1. 查状态 = 一次位运算(不是 hash 查找)。Load barrier 开销极小
  2. 不同 GC 周期用不同标记位(M0/M1 交替)→ 不用清空标记
  3. 重映射状态内置 → 并发重定位安全

负载屏障(Load Barrier)

java
// 每个对象字段访问实际变成了:
Object x = obj.field;
// JVM 内部实际: x = load_barrier(obj.field);

Object load_barrier(Object ref) {
    if (ref.color == NEEDS_FIX) {      // 快速路径:位运算检查
        ref = fix_up(ref);             // 慢速路径:重映射至新地址
    }
    return ref;
}

原理:每次 obj.field 读取时,检查指针的颜色位。如果颜色显示"这个指针需要处理"(比如对象已经被移到新地址),则进入慢速路径更新指针。否则原样返回。

这不是 STW——应用线程继续运行,每个 load 只多一次位运算检查。命中慢速路径时,fix-up 也只更新这一个指针,不会暂停。

全并发架构

ZGC 的并发阶段:
                                              重映射(懒) ← 随下次GC的
              ┌──────────┐                            负载屏障自动完成
              │ STW:     │
              │ Root扫描  │
              │ (<0.1ms) │
              └────┬─────┘

              ┌────▼─────┐    ┌──────────────┐    ┌─────────────────┐
              │ 并发标记   │    │ STW:          │    │  并发重定位       │
              │ (所有线程) │    │ 标记结束+准备   │    │  (所有线程运行中) │
              │ 三色标记   │───→│ 重定位       │───→│  移动对象        │
              │ 彩色指针   │    │ (<0.1ms)     │    │  更新转发表      │
              │ 不会漏标   │    └──────────────┘    │  负载屏障修正    │
              └───────────┘                         └─────────────────┘

每个周期实际只有 2 个 STW 暂停,各 < 0.1ms:根扫描 + GC 结束准备。标记和重定位都是并发的。

2.3 分代 ZGC(JDK 21+)

为什么需要分代?

  • 绝大多数对象(80-95%)在 Young GC 中就死亡了
  • 不分代的 ZGC 每次都要标记整个堆 → 对短期对象扫描浪费
  • 分代后:Young GC 只扫描年轻代 + Remembered Set → 10-20% 吞吐量提升
分代 ZGC 的堆:
┌─────────────────────────────────────────────────────────────────────┐
│  ZGC Heap                                                          │
│  ┌──────────────────┐  ┌──────────────────────────────────────────┐ │
│  │    Young Region   │  │     Old Region                           │ │
│  │  (频繁 GC)        │  │  (少量 GC)                               │ │
│  │  快速标记+重定位   │  │  并发标记+重定位                          │ │
│  └──────────────────┘  └──────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │  Remembered Set: Young 到 Old 的跨代引用 (避免扫描整个 Old)     │ │
│  └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

三、图解全景

ZGC vs G1 停顿对比 (64GB 堆):

停顿时间 (ms):
100 │                                           G1
 50 │                                          ██
 20 │                               G1        ██
 10 │                              ██         ██
  5 │                   G1        ██
  1 │    G1            ██         ZGC
  0.5│   ██            ZGC        ZGC         ZGC
  0.1│   ZGC
     └─────────────────────────────────────────────
          Root扫描      标记       重定位        汇总

四、面试视角

面试题核心回答
ZGC 的彩色指针是什么?64 位指针中用 4 位存储 GC 状态(M1/M0/Rmtd/Fin),指针本身即 metadata,负载屏障通过位运算检查
ZGC 为什么能做到亚毫秒停顿?只有根扫描和阶段收尾是 STW(<0.1ms),标记和重定位全是并发的。负载屏障让应用线程在重定位时自己修正指针
ZGC 和 G1 的区别?G1 有 STW 重定位(堆越大停顿越长),依赖 RSet 和 SATB;ZGC 全并发,彩色指针 + 负载屏障,适合大堆和低延迟场景。ZGC 内存开销稍高(存储转发表)
分代 ZGC 的优势?不分代每次要标记整个堆。分代后 Young GC 只扫描年轻代 + Remembered Set,吞吐量提升 10-20%,降低年轻对象回收延迟
ZGC 有什么缺点?内存占用稍高(转发表)、对 CPU 有额外负载(所有 load 经过 barrier)、大堆场景优势更明显、小于 4GB 的堆不如 G1

追问预判

  • : ZGC 的彩色指针为什么用 42 位地址?

    • : 64 位中 4 位用作颜色、剩余若干位保留,42 位可寻址 4TB 堆。地址位是 4×4TB 的倍数地址映射(多重映射技术),不是纯数学计算
  • : ZGC 的负载屏障和 G1 的写前屏障有什么区别?

    • : ZGC 是读屏障(load barrier),G1 是写屏障(SATB + 日志记录)。ZGC 用读屏障在并发重定位时修正引用,G1 用写屏障维持 SATB 快照。ZGC barrier 开销更低(读路径更常见)
  • : ZGC 适合多大堆?

    • : 官方范围 8MB-16TB。实测建议 >4GB 有明显优势。小于 4GB 的堆,G1 因 barrier 开销更低而更合适

📚 相关链接

  • **垃圾收集器详解(Serial到ZGC)** — GC 收集器全景
  • **GC基本算法(标记清除与复制与标记整理)** — 三色标记算法基础
  • **虚拟线程原理** — 另一个"全并发"前沿特性,了解 ZGC 后对比虚拟线程的并发策略
  • ← 返回 **前沿索引**

Knowledge4J — Java 知识库