← 返回题目列表

ZGC 是什么?为什么它能做到亚毫秒级停顿?染色指针和读屏障是怎么回事?

困难 第 32 / 34 题 更新于 2026/07/26
ZGC染色指针读屏障并发转移

简化版

ZGC 是 JDK 11+ 的超低延迟收集器,目标是无论堆多大(几十 GB 到 TB),STW 停顿都控制在亚毫秒级(<1ms 或几毫秒)。它做到这点的关键是把 GC 里最耗时的两件事——对象转移(复制)和引用更新——也变成并发(和业务线程一起做),而不像 G1 那样在 STW 里做。实现靠两大黑科技:① 染色指针(Colored Pointers)——把 GC 状态信息直接编码进对象指针的高位,一看指针就知道对象状态;② 读屏障(Load Barrier)——每次读引用时插一段检查逻辑,发现对象被移动了就当场修正指针。停顿几乎与堆大小无关,这是 ZGC 最大的卖点。

详细版

ZGC 的核心目标

G1 的停顿随堆增大而增长(要标记/复制的对象更多)
ZGC 的停顿几乎恒定(<1ms~几ms),与堆大小、存活对象数无关
代价:吞吐略低于 G1(读屏障有开销)、需要更多内存

两大关键技术

技术作用
染色指针 Colored Pointers在 64 位指针的高位存 GC 元数据(Marked0/Marked1/Remapped/Finalizable 等),不用额外对象头或卡表
读屏障 Load Barrier每次从堆里读引用时,检查指针的染色位,若对象已被转移则「自愈」——更新为新地址

并发转移(Concurrent Relocation):ZGC 把「复制存活对象到新位置」也并发化——业务线程读到一个「已被移动」的对象引用时,读屏障立即把引用修正到新地址(不用等 GC 统一修正),所以 GC 转移和业务访问能同时进行。

大致流程(几乎全并发,STW 只在阶段切换的极短瞬间):

并发标记 → 并发预备转移(选要回收的 Region) → 并发转移(复制存活对象)
→ 并发重映射(修正指针,可合并到下轮标记)
每个阶段之间有极短 STW(进入/退出安全点),但停顿不随堆增大

⚠️ ZGC 的停顿「与堆大小无关」,是因为它把随堆规模增长的工作(标记、转移、重映射)都放到了并发阶段,STW 只做「切换阶段、扫描线程栈根」等固定量小工作。这是它和 G1「停顿随回收 Region 数增长」的本质区别。

完整版教学

一、ZGC 要突破什么:G1 停顿的天花板

G1 已经能「可预测停顿」,但它有个天花板:停顿时间随堆增大而增长。因为 G1 的 Young GC 和 Mixed GC 里,「复制存活对象 + 更新引用」是在 STW 中做的——堆越大、存活对象越多,这部分 STW 就越长。几十 GB 的大堆下,G1 停顿可能到几百毫秒。

ZGC 的目标是彻底打破这个关联:让停顿时间和堆大小、存活对象数完全解耦,无论 4GB 还是 4TB,STW 都是亚毫秒级。要做到这点,就必须把 G1 还放在 STW 里的「对象转移和引用更新」也搬到并发阶段。而这带来一个核心难题:业务线程和 GC 同时访问、移动对象时,怎么保证业务读到的引用永远指向正确的位置? 染色指针 + 读屏障就是答案。

二、染色指针:把 GC 状态写进指针本身

传统 GC 把对象的 GC 状态(是否标记、是否移动)存在对象头或额外的数据结构(卡表、位图)里。ZGC 的创新是:直接把状态编码进指针的高位

64 位地址空间,实际只用 42~46 位寻址(几十 TB 足够),高位空着:
┌──────────┬────────────────────────────────┐
│ 染色位   │        对象实际地址(42位)         │
│(4 bit)   │                                  │
└──────────┴────────────────────────────────┘
染色位含义:Marked0 / Marked1(标记状态)/ Remapped(已重映射)/ Finalizable
一看指针高位,就知道这个对象"标记了没、移动了没",无需查别处

好处:GC 状态和指针一体,读指针即知状态,省去卡表、对象头标记位等额外结构,也让「判断对象是否需要处理」变成一次廉价的位运算。代价是占用了指针的高位(所以 ZGC 需要 64 位系统,且早期不支持压缩指针)。染色指针是 ZGC 一切并发能力的信息基础。

三、读屏障:引用的「自愈」机制

染色指针解决了「怎么知道对象状态」,读屏障解决「读到脏引用怎么办」。读屏障是每次从堆中读取一个对象引用时,自动插入的一小段检查代码

业务代码: Object o = obj.field;   // 读一个引用
ZGC 在这行悄悄插入读屏障:
  1. 读出引用(带染色位)
  2. 检查染色位:这个对象被移动过吗?
  3. 若"已被转移"(染色位显示 relocated):
     → 当场查转发表(forwarding table),拿到新地址
     → 把 obj.field 就地更新为新地址("自愈"/self-healing)
     → 返回新地址给业务代码
  4. 若正常:直接返回

关键在「自愈」:业务线程读到一个指向旧位置的引用时,读屏障立即把它修正到新位置,并把这个修正写回原字段——引用被谁读到,谁就顺手修好它。这样 GC 不用在 STW 里统一更新所有引用(那才是长停顿的根源),而是「懒惰地、并发地、由业务访问驱动」逐步修正。这就是 ZGC 能并发转移对象的核心。代价是每次读引用都多一点开销(读屏障),所以 ZGC 吞吐比 G1 略低。

四、并发转移:复制对象也不停顿

有了染色指针 + 读屏障,ZGC 就能做到 G1 做不到的事——并发转移(复制存活对象到新 Region)

GC 线程:把 Region A 的存活对象复制到 Region B,在转发表记录 "旧地址→新地址"
        同时把旧引用的染色位标记为 "已转移"

业务线程(同时运行):读到指向 A 中对象的旧引用
        → 读屏障发现"已转移" → 查转发表 → 拿到 B 的新地址 → 自愈
        → 业务读到的永远是正确的新对象

两者并发,互不阻塞:GC 在搬,业务在读,读屏障保证一致性

对比 G1:G1 的对象复制和引用更新在 STW 里做(停顿随对象数增长);ZGC 把它们并发化,STW 只做「扫描线程栈的根引用」这种固定量的小工作。所以 ZGC 的 STW 停顿只和线程数相关,与堆大小无关——这是它亚毫秒停顿的根本原因。

五、ZGC vs G1 vs CMS:低延迟的代价

维度CMSG1ZGC
停顿目标低(但不可控)可预测(随堆增长)亚毫秒(与堆无关)
对象转移不转移(碎片)STW 中转移并发转移
关键技术增量更新写屏障RSet + SATB染色指针 + 读屏障
适用堆中小堆大堆超大堆(几十 GB~TB)
吞吐略低(读屏障开销)
JDK已移除9+ 默认11+(15 生产可用)

选型逻辑很清晰:停顿是硬指标、堆特别大时选 ZGC(如实时交易、大缓存、超大堆服务);追求吞吐、堆中等大小选 G1(大多数场景)。ZGC 不是「更好的 G1」,而是「用一点吞吐换极致低延迟」——如果你的系统能容忍 G1 的几十毫秒停顿,就没必要上 ZGC。

六、ZGC 的演进与适用判断

ZGC 从 JDK 11 实验性引入,JDK 15 生产可用,之后持续增强(如支持分代 ZGC,JDK 21 的 Generational ZGC 大幅提升吞吐):

什么时候用 ZGC:
  ✓ 堆很大(>16GB,尤其几十 GB 到 TB)
  ✓ 对停顿极度敏感(要求 P99 延迟稳定在几毫秒内)
  ✓ 能接受略低的吞吐和更高的内存占用

什么时候不用:
  ✗ 堆不大、G1 停顿已能满足 → 用 G1 更省资源
  ✗ 追求极致吞吐的批处理 → 用 Parallel GC

一个重要认知:低延迟收集器不是免费午餐。ZGC 用读屏障(每次读引用的开销)、更多内存(多份地址视图/转发表)换来了极低停顿。它是给「大堆 + 严苛延迟」场景的专用武器,不是通用最优解。

记忆钩子:「ZGC = 亚毫秒停顿且与堆大小无关;靠染色指针(GC 状态编进指针高位,读指针即知状态)+ 读屏障(读引用时检查,已转移就自愈到新地址)实现并发转移;把 G1 放在 STW 的复制和引用更新搬到并发;代价是吞吐略低、内存略高」

七、常见误区与追问

  • 误区:ZGC 完全没有 STW。 仍有极短 STW(进入/退出安全点、扫描线程栈根),只是这些工作量固定、不随堆增大,所以停顿稳定在亚毫秒级。
  • 误区:ZGC 一定比 G1 好,应该都用它。 ZGC 吞吐略低、内存占用更高;堆不大、G1 停顿已满足时用 G1 更划算,ZGC 是「大堆+严苛延迟」的专用方案。
  • 误区:染色指针存在对象头里。 它存在指针本身的高位(不是对象头),一看引用就知道对象 GC 状态,省去额外结构。
  • 误区:读屏障和写屏障一样。 读屏障在「读引用」时触发(ZGC 用它做转移自愈);写屏障在「写引用」时触发(CMS/G1 用它维护标记/RSet),触发时机和用途不同。
  • 追问:ZGC 为什么停顿和堆大小无关? 它把随堆规模增长的工作(标记、对象转移、引用重映射)都放到并发阶段,STW 只做扫描线程栈根等固定量小工作,所以停顿只和线程数相关。
  • 追问:读屏障的自愈是什么意思? 业务线程读到一个指向已被移动对象的旧引用时,读屏障当场查转发表拿到新地址、并把该字段就地更新为新地址,「谁读到谁顺手修好」,无需 GC 统一 STW 更新。
  • 追问:ZGC 为什么需要 64 位系统、不支持压缩指针(早期)? 因为它要用指针的高位存染色信息,需要足够的地址位;压缩指针会占用这些位,所以早期 ZGC 与之冲突。

八、加强记忆

ZGC 是 JDK 11+ 的超低延迟收集器,目标是「无论堆多大,STW 停顿都稳定在亚毫秒级」。它突破 G1「停顿随堆增长」天花板的方法,是把 G1 仍放在 STW 里的对象转移(复制)和引用更新也并发化。实现靠两大技术:① 染色指针——把 GC 状态(标记/已转移等)编码进 64 位指针的高位,读指针即知对象状态,省去卡表/对象头标记;② 读屏障——每次从堆读引用时插一段检查,发现对象被移动就当场查转发表拿新地址并自愈(就地更新该字段),「谁读到谁修好」。二者配合实现并发转移:GC 搬对象、业务同时读,读屏障保证读到的永远是正确新地址,无需 STW 统一更新引用——所以 STW 只做「扫描线程栈根」这种固定量小活,停顿与堆大小解耦。代价是读屏障降低吞吐、内存占用更高,所以 ZGC 是「大堆(几十 GB~TB)+ 严苛延迟」的专用武器,堆不大时 G1 更划算。一句话「染色指针记状态、读屏障读时自愈、并发转移不停顿、停顿与堆无关但吞吐略低」。