专栏文章
专栏文章
GC 算法系列
1. GC 算法 #01:三色标记与写屏障 2. GC 算法 #02:分代 GC 3. GC 算法 #03:G1 GC(Region-based) 4. GC 算法 #04:ZGC(染色指针与负载屏障) 5. GC 算法 #05:引用计数与循环引用检测

GC 算法 #04:ZGC(染色指针与负载屏障)

发布于 2026-06-02 02:40 👁 10 次阅读
#GC#JVM#算法#ZGC#engineering#colored-pointers#load-barrier

ZGC(Z Garbage Collector)是 JDK 11 引入的低延迟 GC,目标是任意堆大小下暂停时间 < 10ms。核心创新是染色指针(Colored Pointers):将 GC 元数据直接编码进对象指针,通过**负载屏障(Load Barrier)**在读取指针时动态修正,实现几乎完全并发的标记和压缩。

相关文章三色标记与写屏障 · 分代 GC · G1 GC(Region-based)


目录

章节 说明
核心挑战:并发压缩 为什么其他 GC 做不到并发移动对象
染色指针 4 个元数据位的含义
负载屏障 读指针时的修正逻辑
ZGC 的完整流程 3 次极短 STW + 全程并发
完整伪代码 标记 / 重定位 / 负载屏障
执行追踪 一次完整 ZGC 循环
异常场景 指针位耗尽 / 并发 Allocation Stall
ZGC vs G1 vs Shenandoah 技术路线对比

核心挑战:并发压缩

所有低延迟 GC 都要解决的根本问题:
  GC 在移动对象的同时,用户程序也在读写这些对象
  如果 GC 把对象从地址 A 移到地址 B:
    用户程序读 A → 读到旧地址 → 用旧指针访问 → 数据错误或崩溃

传统解法(G1 / CMS):
  STW(暂停用户程序)期间才移动对象
  → 安全,但暂停时间随存活对象数线性增长

ZGC 的解法:
  利用染色指针 + 负载屏障:
  "当用户程序读到旧指针时,负载屏障自动修正为新地址"
  → 用户程序无感知,GC 和用户程序真正并发运行

染色指针

zgc colored pointers

64-bit 指针的空间借用

x86-64 架构的虚拟地址空间:
  理论上 64 bit,但实际 CPU 只使用低 47 bit(128 TB 用户空间)
  高 17 bit 和次高 4 bit 是冗余的(OS 不使用)

ZGC 借用 4 个 bit(第 42~45 bit)存储元数据:

  63       46 45 44 43 42 41            0
  ├─────────┤──┤──┤──┤──┤──────────────┤
  │  未使用  │fin│rel│M1 │M0 │ 对象地址  │
                ↑   ↑   ↑   ↑
             FinalizableMark
                 Remapped
                     Marked1
                         Marked0

Marked0 / Marked1:标记位,两位交替使用(GC 周期间轮换)
Remapped:已重映射(对象已移动到新地址,指针已更新)
Finalizable:对象只有 Finalizer 可达(待 Finalize)

为什么需要两个标记位(M0 和 M1)?

GC 周期 N:使用 Marked0 表示"存活"
GC 周期 N+1:使用 Marked1 表示"存活"
  交替使用,避免清除标记的代价(只需要更换"当前有效位"的定义)

对比三色标记:
  传统 GC:白色/灰色/黑色 存在对象头(需要 STW 重置)
  ZGC:颜色存在指针中(更换有效位,O(1) 全局重置)

负载屏障

负载屏障在读取对象指针时执行(G1 写屏障在写时执行,ZGC 是读时):

# 编译器在每次 obj.field 读取处插入:
# (只对堆引用插入,不对原始类型、null 等插入)

def load_barrier(obj, field_offset) -> pointer:
    ptr = raw_load(obj, field_offset)    # 从内存直接读指针

    if ptr is null:
        return null

    if ptr.color == EXPECTED_COLOR:      # 颜色正确(本 GC 周期的有效颜色)
        return ptr                       # 快路径,几乎无开销

    # 慢路径:颜色不对,需要修正
    return slow_path_fix(ptr)

def slow_path_fix(ptr):
    # 情况1:标记阶段,指针未被标记
    if not ptr.is_marked():
        gray_queue.push(ptr)             # 加入标记队列(帮助 GC 标记)
        return mark(ptr)                 # 返回标记后的指针

    # 情况2:重定位阶段,指针未被更新(对象已移动,但指针还指旧地址)
    if not ptr.is_remapped():
        new_ptr = forwarding_table.lookup(ptr)   # 查转发表
        cas_update(obj, field_offset, ptr, new_ptr)  # 原子更新指针
        return new_ptr

    return ptr

负载屏障的代价

每次读堆引用都执行检查(但快路径极短,约 2~3 条额外指令)
G1 对比(写屏障):
  写比读少,写屏障影响的代码路径更少
  但 ZGC 负载屏障通过颜色检查快速跳过慢路径,实际开销可控

测量:ZGC 负载屏障带来约 10~15% 的吞吐量损失(vs G1 约 5~10%)

ZGC 的完整流程

ZGC 的 GC 周期(大部分工作并发,3 次极短 STW):

Phase 1: Pause Mark Start(STW,~1ms)
  标记所有 GC Roots(线程栈、JNI 引用等)
  颜色:切换"当前标记颜色"(Marked0 ↔ Marked1 交替)

Phase 2: Concurrent Mark(并发,~秒级)
  遍历所有存活对象,将指针染为"当前标记颜色"
  负载屏障帮助:用户程序读到未标记的指针时,会帮助 GC 标记
  → GC 和用户程序协作完成标记

Phase 3: Pause Mark End(STW,~1ms)
  处理 GC 线程的残余工作(少量 SATB 缓冲区)
  确保所有存活对象都被标记

Phase 4: Concurrent Prepare for Relocation(并发)
  分析各 ZPage 的存活率
  选择要回收的 ZPage(与 G1 类似,选垃圾最多的)
  为选中的 ZPage 构建转发表(Forwarding Table)

Phase 5: Pause Relocate Start(STW,~1ms)
  标记 GC Roots 对应的 ZPage 开始重定位
  (只是"开始"的标记,实际复制在并发阶段完成)

Phase 6: Concurrent Relocate(并发)
  GC 线程将存活对象从旧 ZPage 复制到新 ZPage
  转发表记录旧地址 → 新地址的映射
  同时:用户程序通过负载屏障的慢路径自助更新指针

Phase 7: Concurrent Remap(并发,下一周期的一部分)
  扫描所有堆引用,将剩余旧指针更新为新地址
  清除转发表
  完成后,当前 ZPage 可以完全回收

完整伪代码

ZPage 和转发表

class ZPage:
    start:   int      # 页起始地址
    end:     int      # 页结束地址
    live_map: bitset  # 哪些对象存活(并发标记时维护)

class ForwardingTable:
    # 哈希表:旧地址 → 新地址
    def lookup(old_addr) -> new_addr: ...
    def insert(old_addr, new_addr): ...

Concurrent Relocate(核心)

def concurrent_relocate(zpages_to_relocate):
    for zpage in zpages_to_relocate:
        new_zpage = allocate_new_zpage()
        ft = ForwardingTable()
        forwarding_tables[zpage.start] = ft

        for obj in zpage.live_objects():
            # 复制对象到新页
            new_addr = new_zpage.allocate(obj.size())
            copy(obj, new_addr)
            ft.insert(obj.address, new_addr)

            # 在旧地址安装转发指针(用于负载屏障查询)
            obj.install_forwarding_ptr(new_addr)

        # 旧 ZPage 标记为"可重用"(但转发表还在)
        zpage.release()

负载屏障慢路径(完整版)

def slow_path_fix(ptr, obj, field_offset):
    # 提取真实地址(去掉颜色位)
    real_addr = ptr.strip_color_bits()

    # 查询转发表(对象是否已被移动?)
    ft = forwarding_tables.get(page_of(real_addr))
    if ft is not None:
        new_addr = ft.lookup(real_addr)
        if new_addr is not None:
            # 对象已移动,修复指针
            new_ptr = new_addr | CURRENT_COLOR_BITS
            # CAS 原子更新:避免并发重复修复
            cas_update(obj, field_offset, ptr, new_ptr)
            return new_ptr

    # 对象未被移动,只需染色
    new_ptr = real_addr | CURRENT_COLOR_BITS
    cas_update(obj, field_offset, ptr, new_ptr)
    return new_ptr

执行追踪

32GB 堆,1000 个 ZPage(32MB/page),目标暂停 < 10ms

T=0ms: Pause Mark Start(STW=1ms)
       标记 GC Roots,切换标记颜色从 M0 → M1

T=1ms ~ T=800ms: Concurrent Mark(并发约 800ms)
  GC 线程遍历对象图,将存活对象的指针标记为 M1
  用户程序读到 M0 指针 → 负载屏障慢路径 → 帮助标记为 M1
  进度:99% 的对象被 GC 线程标记,1% 靠负载屏障

T=800ms: Pause Mark End(STW=1ms)
  处理残余标记任务

T=801ms ~ T=1000ms: Concurrent Prepare(并发)
  分析:50 个 ZPage 死亡率 > 80%(选出待回收集)
  为这 50 个 ZPage 构建转发表

T=1000ms: Pause Relocate Start(STW=1ms)
  标记 Root 对应的 ZPage 开始重定位

T=1001ms ~ T=1500ms: Concurrent Relocate(并发)
  GC:将 50 个 ZPage 的存活对象复制到新 ZPage(约 1.6GB 数据移动)
  用户程序:每次读到旧指针,负载屏障自动修正为新地址
  用户无感知!

T=1500ms: 全程 STW 总计 = 1+1+1 = 3ms ✅(远低于 10ms 目标)

异常场景

指针颜色位耗尽(64-bit 限制)

ZGC 借用 64-bit 指针的第 42~45 位(4 个 bit)
→ 最大可寻址空间 = 2^42 = 4 TB

如果堆 > 4 TB:无法使用 ZGC(颜色位空间不够)

JDK 16+ 改进(Generational ZGC):
  使用页级别的映射替代指针着色
  不再受 4 TB 限制

Allocation Stall(分配阻塞)

场景:并发 GC 速度 < 对象分配速度
  → Free ZPage 耗尽 → 新对象无法分配
  → 用户线程被迫等待 GC 释放空间

检测:GC 日志中 "Allocation Stall" 事件

解法:
  增大堆(给 GC 更多缓冲空间)
  降低 ZAllocationSpikeTolerance(提前触发 GC)
  降低分配速率(减少短命大对象)

ZGC vs G1 vs Shenandoah

维度 G1 ZGC Shenandoah
STW 暂停 可控(目标 200ms) < 10ms < 10ms
吞吐量 中(~10% 损失) 中(~15% 损失)
内存开销 RSet(5~10%) 转发表(较小) 转发指针(1 word/obj)
并发压缩 ❌ STW 复制 ✅ 并发重定位 ✅ 并发疏散
原理 写屏障 染色指针 + 读屏障 Brooks 转发指针 + 读屏障
适用场景 通用,大堆 超低延迟,TB 级堆 低延迟,中等堆
JDK 版本 9+ 默认 15+ 正式 15+ 正式

参考资料

  • Liden, P. & Karlsson, B. (2018). The Z Garbage Collector - An Introduction
  • JEP 333: ZGC - https://openjdk.org/jeps/333
  • JEP 376: ZGC Concurrent Thread-Stack Processing - https://openjdk.org/jeps/376
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论