Skip to content
Go back

ZGC——着色指针与读屏障实现亚毫秒级停顿

ZGC:着色指针 + 读屏障 = 停顿 <1ms

一句话结论(30s)

ZGC 的本质是把 GC 元数据编码进指针高 4 位的”着色指针”,再配合读屏障把对象转移从 STW 变成并发,因为指针自带 Remapped/Marked 状态,应用线程读到已移动对象时可自行修正引用(自愈)。关键设计是读屏障自愈 + Marked0/Marked1 交替使用免重置,实现停顿 <1ms 且不随堆增长。权衡在于指针让出 4 位、实际寻址降到约 8TB,且每次读引用都要过一次屏障。

核心原理(2min)

主流程:ZGC 把 64 位指针高 4 位用于 GC 元数据(Finalizable/Remapped/Marked0/Marked1),应用线程读对象引用时读屏障检查这些位——对象已移动则 remap 修正,处于标记阶段则标记并可能转移,同时把修正后的地址写回字段(自愈)。关键机制:读屏障自愈让 GC 线程批量转移对象时应用线程无需 STW,Marked0/Marked1 交替避免每轮重置标记位;代价是寻址能力从 64 位降到约 43 位(8TB)。

底层深入(5-10min)

ZGC 的设计目标

G1 的停顿随堆大小线性增长(200ms @ 几十 GB 堆)。ZGC 的目标:停顿时间不随堆大小增长,始终 <1ms。

着色指针(Colored Pointers)

ZGC 把 GC 元数据编码到 64 位指针的高 4 位:

64 位指针布局:
[4 bit GC metadata][未使用][46 bit 实际地址]

4 bit metadata:
  Finalizable  - 对象有 finalize 方法
  Remapped     - 引用已指向新位置
  Marked0      - 标记状态 0
  Marked1      - 标记状态 1(交替使用,无需重置标记)

💭 思考:为什么标记位要准备 Marked0 / Marked1 两个,而不是一个位每轮清零?——标记是每轮 GC 都要做的,如果只有一个标记位,每轮结束都得遍历整个堆把所有对象的标记位清零,堆越大这笔”重置”越贵。准备两个位轮流用,本轮用 Marked0、下轮用 Marked1,直接换位就相当于”清空”,完全省掉全堆重置。这正是 ZGC 追求”停顿不随堆增长”的一个缩影——连标记重置这种 O(堆) 的活都要绕开。

这里先问一句:ZGC 为什么非要把 GC 元数据塞进指针,而不是像 G1 那样写在对象头里? 因为 ZGC 要做”并发转移”——对象在应用线程还在跑的时候就被搬走,如果状态只放在对象头里,应用线程拿到一个旧地址时无从判断”这个对象是否已被搬走、新家在哪”。把状态编码进指针,应用线程每次读引用时看一眼指针自己带的位,就能当场发现”这个对象移动过”并修正,这才让”无 STW 的并发转移”成为可能。想通这一点,再看下面的读屏障就顺了。

代价:实际寻址能力从 64 位降到 ~46 位(64TB 地址空间)。Linux 默认只给用户态 47 位地址空间,ZGC 再占 4 位 → 实际可用约 43 位(8TB),对绝大多数应用够用。

💭 思考:为什么 ZGC 愿意让出 4 位寻址空间,把 64 位降到约 8TB?——要从它想换到什么反推:让出这 4 位,换来”指针自带 GC 状态”,再配合读屏障自愈,才能把对象转移从 STW 变成并发,最终拿到”停顿 <1ms 且不随堆增长”。而 8TB 对绝大多数应用早已够用,G1 在几十 GB 堆上就 200ms 停顿的真实痛点却天天可见。用一个几乎用不满的资源,去换一个高频痛点,这笔买卖显然划算;反过来,若死守 64 位寻址,就只能像 G1 那样 STW 转移,停顿随堆线性增长。

当前主线源码(JDK 21+ 分代 ZGC)的真实位布局——定义在 zAddress.hpp。分代 ZGC 把元数据位从经典设计的高位挪到了指针低位,并扩展为 16 位(RRRRMMmmFFrr0000),逐字摘录如下:

// Layout of metadata bits in colored pointer / zpointer.
//
// A zpointer is a combination of the address bits (heap base bit + offset)
// and two low-order metadata bytes, with the following layout:
//
// RRRRMMmmFFrr0000
// ****               : Used by load barrier
// **********         : Used by mark barrier
// ************       : Used by store barrier
//             ****   : Reserved bits

constexpr uintptr_t z_pointer_mask(size_t shift, size_t bits) {
  return (((uintptr_t)1 << bits) - 1) << shift;
}

constexpr uintptr_t z_pointer_bit(size_t shift, size_t offset) {
  return (uintptr_t)1 << (shift + offset);
}

// Reserved bits
const size_t      ZPointerReservedShift   = 0;
const size_t      ZPointerReservedBits    = 4;
const uintptr_t   ZPointerReservedMask    = z_pointer_mask(ZPointerReservedShift, ZPointerReservedBits);

const uintptr_t   ZPointerReserved0       = z_pointer_bit(ZPointerReservedShift, 0);
const uintptr_t   ZPointerReserved1       = z_pointer_bit(ZPointerReservedShift, 1);
const uintptr_t   ZPointerReserved2       = z_pointer_bit(ZPointerReservedShift, 2);
const uintptr_t   ZPointerReserved3       = z_pointer_bit(ZPointerReservedShift, 3);

// Remembered set bits
const size_t      ZPointerRememberedShift = ZPointerReservedShift + ZPointerReservedBits;
const size_t      ZPointerRememberedBits  = 2;
const uintptr_t   ZPointerRememberedMask  = z_pointer_mask(ZPointerRememberedShift, ZPointerRememberedBits);

const uintptr_t   ZPointerRemembered0     = z_pointer_bit(ZPointerRememberedShift, 0);
const uintptr_t   ZPointerRemembered1     = z_pointer_bit(ZPointerRememberedShift, 1);

// Marked bits
const size_t      ZPointerMarkedShift     = ZPointerRememberedShift + ZPointerRememberedBits;
const size_t      ZPointerMarkedBits      = 6;
const uintptr_t   ZPointerMarkedMask      = z_pointer_mask(ZPointerMarkedShift, ZPointerMarkedBits);

const uintptr_t   ZPointerFinalizable0    = z_pointer_bit(ZPointerMarkedShift, 0);
const uintptr_t   ZPointerFinalizable1    = z_pointer_bit(ZPointerMarkedShift, 1);
const uintptr_t   ZPointerMarkedYoung0    = z_pointer_bit(ZPointerMarkedShift, 2);
const uintptr_t   ZPointerMarkedYoung1    = z_pointer_bit(ZPointerMarkedShift, 3);
const uintptr_t   ZPointerMarkedOld0      = z_pointer_bit(ZPointerMarkedShift, 4);
const uintptr_t   ZPointerMarkedOld1      = z_pointer_bit(ZPointerMarkedShift, 5);

// Remapped bits
const size_t      ZPointerRemappedShift   = ZPointerMarkedShift + ZPointerMarkedBits;
const size_t      ZPointerRemappedBits    = 4;
const uintptr_t   ZPointerRemappedMask    = z_pointer_mask(ZPointerRemappedShift, ZPointerRemappedBits);

const uintptr_t   ZPointerRemapped00      = z_pointer_bit(ZPointerRemappedShift, 0);
const uintptr_t   ZPointerRemapped01      = z_pointer_bit(ZPointerRemappedShift, 1);
const uintptr_t   ZPointerRemapped10      = z_pointer_bit(ZPointerRemappedShift, 2);
const uintptr_t   ZPointerRemapped11      = z_pointer_bit(ZPointerRemappedShift, 3);

// Barrier metadata masks
const uintptr_t   ZPointerLoadMetadataMask  = ZPointerRemappedMask;
const uintptr_t   ZPointerMarkMetadataMask  = ZPointerLoadMetadataMask | ZPointerMarkedMask;
const uintptr_t   ZPointerStoreMetadataMask = ZPointerMarkMetadataMask | ZPointerRememberedMask;
const uintptr_t   ZPointerAllMetadataMask   = ZPointerStoreMetadataMask;

// Colored oop
enum class zpointer            : uintptr_t { null = 0 };

// Uncolored oop - safe to dereference
enum class zaddress            : uintptr_t { null = 0 };

分析:这里最关键的是 zpointer(带颜色的 oop)和 zaddress(去掉颜色、可安全解引用的真实地址)被定义为两个不同的枚举类型,从类型系统上就杜绝了”拿带着 GC 位指针直接解引用”。每个元数据位都用 z_pointer_mask(shift, bits) 按”位移 + 位宽”计算,位段之间靠 shift 首尾相接、互不重叠,所以同一根指针里能同时容纳 Reserved/Remembered/Marked(含 Finalizable、MarkedYoung、MarkedOld)/Remapped 多组状态。经典 ZGC 把 4 个元数据位放在高位,分代 ZGC 为了配合 x86 上单条移位指令把颜色挪到低位并扩到 16 位——但”指针本身携带 GC 状态”这一核心思想完全一致。

读屏障(Load Barrier)

应用线程读对象引用时,读屏障自动检查指针的 GC 元数据:

// ZGC 读屏障伪码
Object* load_barrier(Object** field) {
    Object* ptr = *field;
    if (ptr & REMAPPED_MASK)  // 对象已移动
        return remap(ptr);     // 修正为正确的地址
    if (ptr & MARKED_MASK)    // 对象在标记阶段
        return mark_and_relocate(ptr);  // 标记 + 可能移动
    return ptr;                // 正常引用
}

真实实现分布在两个文件:zBarrierSet.inline.hpp 负责按引用强度(强/弱/虚)分发,zBarrier.inline.hpp 负责快路径判断 + 慢路径 relocate/remap + 自愈写回。逐字摘录如下:

// zBarrierSet.inline.hpp —— 读屏障入口,按引用强度分发
template <DecoratorSet decorators, typename BarrierSetT>
inline zaddress ZBarrierSet::AccessBarrier<decorators, BarrierSetT>::load_barrier(zpointer* p, zpointer o) {
  verify_decorators_absent<ON_UNKNOWN_OOP_REF>();

  if (HasDecorator<decorators, AS_NO_KEEPALIVE>::value) {
    if (HasDecorator<decorators, ON_STRONG_OOP_REF>::value) {
      // Load barriers on strong oop refs don't keep objects alive
      return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o);
    } else if (HasDecorator<decorators, ON_WEAK_OOP_REF>::value) {
      return ZBarrierSet::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o);
    } else {
      assert((HasDecorator<decorators, ON_PHANTOM_OOP_REF>::value), "Must be");
      return ZBarrierSet::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o);
    }
  } else {
    if (HasDecorator<decorators, ON_STRONG_OOP_REF>::value) {
      return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o);
    } else if (HasDecorator<decorators, ON_WEAK_OOP_REF>::value) {
      return ZBarrierSet::load_barrier_on_weak_oop_field_preloaded(p, o);
    } else {
      assert((HasDecorator<decorators, ON_PHANTOM_OOP_REF>::value), "Must be");
      return ZBarrierSet::load_barrier_on_phantom_oop_field_preloaded(p, o);
    }
  }
}
// zBarrier.inline.hpp —— 强引用读屏障:快路径 + 慢路径 + 自愈
inline zaddress ZBarrier::load_barrier_on_oop_field(volatile zpointer* p) {
  const zpointer o = load_atomic(p);
  return load_barrier_on_oop_field_preloaded(p, o);
}

inline zaddress ZBarrier::load_barrier_on_oop_field_preloaded(volatile zpointer* p, zpointer o) {
  auto slow_path = [](zaddress addr) -> zaddress {
    return addr;
  };

  return barrier(is_load_good_or_null_fast_path, slow_path, color_load_good, p, o);
}

template <typename ZBarrierSlowPath>
inline zaddress ZBarrier::barrier(ZBarrierFastPath fast_path, ZBarrierSlowPath slow_path, ZBarrierColor color, volatile zpointer* p, zpointer o, bool allow_null) {
  z_verify_safepoints_are_blocked();

  // Fast path
  if (fast_path(o)) {
    return ZPointer::uncolor(o);
  }

  // Make load good
  const zaddress load_good_addr = make_load_good(o);

  // Slow path
  const zaddress good_addr = slow_path(load_good_addr);

  // Self heal
  if (p != nullptr) {
    // Color
    const zpointer good_ptr = color(good_addr, o);

    assert(!is_null(good_ptr), "Always block raw null");

    self_heal(fast_path, p, o, good_ptr, allow_null);
  }

  return good_addr;
}

inline zaddress ZBarrier::make_load_good(zpointer o) {
  if (is_null_any(o)) {
    return zaddress::null;
  }

  if (ZPointer::is_load_good_or_null(o)) {
    return ZPointer::uncolor(o);
  }

  return relocate_or_remap(ZPointer::uncolor_unsafe(o), remap_generation(o));
}

inline void ZBarrier::self_heal(ZBarrierFastPath fast_path, volatile zpointer* p, zpointer ptr, zpointer heal_ptr, bool allow_null) {
  if (!allow_null && is_null_assert_load_good(heal_ptr) && !is_null_any(ptr)) {
    // Never heal with null since it interacts badly with reference processing.
    // A mutator clearing an oop would be similar to calling Reference.clear(),
    // which would make the reference non-discoverable or silently dropped
    // by the reference processor.
    return;
  }

  assert_is_valid(ptr);
  assert_is_valid(heal_ptr);
  assert(!fast_path(ptr), "Invalid self heal");
  assert(fast_path(heal_ptr), "Invalid self heal");

  assert(ZPointer::is_remapped(heal_ptr), "invariant");

  for (;;) {
    assert_transition_monotonicity(ptr, heal_ptr);

    // Heal
    const zpointer prev_ptr = AtomicAccess::cmpxchg(p, ptr, heal_ptr, memory_order_relaxed);
    if (prev_ptr == ptr) {
      // Success
      return;
    }

    if (fast_path(prev_ptr)) {
      // Must not self heal
      return;
    }

    // The oop location was healed by another barrier, but still needs upgrading.
    // Re-apply healing to make sure the oop is not left with weaker (remapped or
    // finalizable) metadata bits than what this barrier tried to apply.
    ptr = prev_ptr;
  }
}

分析:barrier() 的第一行就是快路径——若 fast_path(o)(即 is_load_good_or_null)成立,直接 ZPointer::uncolor(o) 抹掉颜色位返回真实地址,正常引用几乎零开销。只有元数据位不是 “load good”(对象已移动、需要 remap)时才进慢路径,make_load_good()relocate_or_remap 顺着 forwarding 指针找到对象新位置。而”读时自愈”落在 self_heal():它用 CAS(AtomicAccess::cmpxchg)把重新着色后的新指针写回字段,成功即完成自愈;若已被别的线程抢先治愈,则升级到更强的元数据状态后重试,保证字段最终收敛到 “load good”。

关键创新:读屏障是”自愈”的——第一次读取已移动对象时,读屏障修正引用并更新字段,后续读取都不再经过屏障。这实现了并发转移(转移对象时应用线程仍可运行)。

再停一下:读屏障为什么能”自愈”?为什么修一次就够、而不是每次读都要修正? 因为读屏障在慢路径结束时,会用 CAS 把”重新着色后的新指针”写回原字段——这一写就把”旧地址”换成了”新地址”,之后任何线程再读这个字段拿到的已经是好指针,直接走快路径返回,自然不再过慢路径。这正是 ZGC 能”并发转移”的关键:GC 线程批量搬对象,应用线程第一次撞上旧指针时自己顺手把引用修好,两边谁都不用停下来等对方。

并发转移 vs G1 的 STW 转移

G1 的转移阶段需要 STW——暂停所有应用线程,将存活对象从待回收 Region 拷贝到新 Region。ZGC 的转移是并发的——应用线程读屏障自愈 + GC 线程批量转移,全程无 STW。

这里再追问一层:为什么 G1 的转移必须 STW,而 ZGC 却能做到并发? 因为 G1 没有”着色指针”这种把状态挂在引用本身的手段——它搬对象后,得靠 STW 期间统一修正所有指向旧位置的指针,否则应用线程会拿到失效地址;而 ZGC 把”这个对象搬过没有、新家在哪”的信息编码进指针,应用线程在读的那一刻自行修正,于是转移和修引用都不需要暂停世界。

总结

G1ZGC
停顿随堆增长(200ms @ 几十GB)<1ms 不随堆增长
指针开销0(64 位全用于寻址)4 bit 用于 GC 元数据
转移STW并发(读屏障自愈)
适用堆几 GB ~ 几十 GB几百 GB ~ TB 级

章末提问


Share this post on:

Previous Post
MySQL插入意向锁——间隙锁的并发优化变体
Next Post
Redis Cluster Gossip协议——去中心化的元数据传播