如何解决跨代引用

54 阅读3分钟

如何解决跨代引用

ZGC 分代模型下处理跨代引用的核心创新点——通过 “卡表粗筛 + 并发卡清洗 + RSet 精炼” 的三级协作,将跨代引用处理从 STW 转为并发。以下是完整流程的深度解析:


⚙️ ZGC 跨代引用处理全流程

sequenceDiagram
    participant AppThread as 应用线程
    participant Barrier as 写屏障
    participant CardTable as 卡表
    participant Scrubber as 卡清洗线程
    participant RSet as 全局RSet
    participant MinorGC as Minor GC

    AppThread->>Barrier: 1. 写老年代对象字段(指向年轻代)
    Barrier->>CardTable: 2. 标记对应卡页为 dirty(CAS)
    Scrubber->>CardTable: 3. 并发扫描 dirty 卡页
    Scrubber->>Scrubber: 4. 提取卡页内实际跨代引用地址
    Scrubber->>RSet: 5. 将精确引用加入 RSet
    Scrubber->>CardTable: 6. 清空 dirty 标记
    MinorGC->>RSet: 7. 仅扫描 RSet 中的精确引用
    MinorGC->>MinorGC: 8. 回收年轻代(无老年代扫描)

🔍 关键步骤详解

1. 写屏障:粗粒度标记(纳秒级)

  • 触发条件:老年代对象写入指向年轻代的引用。

  • 动作

    void write_barrier(oop* field, oop new_val) {
      if (is_old(field) && is_young(new_val)) {
        card_table.mark_dirty(field); // CAS 标记卡页
      }
      *field = new_val; // 实际写入
    }
    
  • 开销:仅 1 次 CAS 操作(约 5ns)。

2. 并发卡清洗:脏页到精确引用的转换(微秒级)

2.1执行者:后台 Scrubber 线程(默认 1 个,可配置)。后台一直执行

2.2核心任务

for (oop obj : dirty_card_page) {
  if (is_cross_gen_ref(obj)) { // 检查是否跨代引用
    rset.add(obj); // 加入全局 RSet
  }
}

2.3优化

  • 跳过非引用字段:如 intlong 等原生类型不扫描。
  • 批量处理:连续 dirty 卡页合并扫描(提升缓存命中率)。

2.4触发条件:

graph TD
    A[写屏障标记脏卡] --> B{脏卡积累}
    B -->|达到阈值| C[唤醒卡清洗线程]
    D[Minor GC 即将启动] --> C
  • 脏卡阈值触发:当 dirty 卡页数量超过 XX:ZDirtyCardThreshold=1000(默认 1000 页)时启动。
  • Minor GC 前预热:在 Minor GC 启动前 50ms(可配置),强制启动卡清洗,确保 RSet 最新。
  • 执行线程(独立于 GC 工作线程)

2.5执行线程(独立于 GC 工作线程)


// 卡清洗线程(独立线程池)ScrubberThreadPool pool = new ScrubberThreadPool(
  scrubbing_threads,// 线程数 = max(1, ConcGCThreads/4)
  scrub_task_queue// 脏卡页队列
);

  • 线程归属:专属的 ScrubberThread并发标记线程(ConcGCThreads)。
  • 优先级Low(避免抢占应用线程),可通过 XX:ZScrubberPriority=normal 调整。

3. RSet:存储精确引用(零扫描浪费)

  • 数据结构:全局 指针数组,存储所有 老年代 → 年轻代 的引用地址。

    struct RSet {
      oop* refs[RSET_MAX_SIZE]; // 引用指针数组
      size_t count;
    };
    
  • 内存占用:约 0.01% 堆大小(100GB 堆 ≈ 10MB)。

4. Minor GC:高效回收(亚毫秒级)

  • 扫描范围
    • GC Roots(线程栈、静态变量等)。
    • RSet 中的精确引用(直接定位年轻代对象)。
  • 优势
    • 避免扫描老年代(节省 90%+ 时间)。
    • 无卡页内冗余扫描(RSet 仅含有效引用)。

⚡️ 与 ParNew/CMS 的本质差异

维度ParNew/CMSZGC
标记阶段STW 扫描全卡表(>10ms)并发卡清洗(无 STW)
扫描粒度512字节全页扫(80% 冗余)精确引用扫描(0 冗余)
跨代引用存储无持久化结构(每次全扫)全局 RSet(长效记录)
可扩展性堆越大 STW 越长堆大小与停顿解耦(恒 <1ms)

💎 设计价值:工程智慧的结晶

  1. 脏页标记(卡表)
    • 最小化写屏障开销(5ns/次),避免实时维护复杂结构。
  2. 引用精炼(卡清洗)
    • 并发执行:将 CPU 密集型任务移出 STW 关键路径。
    • 按需精度:仅当卡页被标记时才启动扫描。
  3. 精确处理(RSet)
    • 空间换时间:牺牲少量内存(0.01% 堆),换取 Minor GC 确定性低延迟。

​最终结论​​:

ZGC 通过 “粗标记 → 精提取 → 高效扫” 的三级流水线,将跨代引用处理转化为 并发任务,使 Minor GC 停顿与堆大小彻底解耦。这是其实现 TB 堆亚毫秒级回收的核心基石,也是相比 ParNew/CMS 的代际突破。