如何解决跨代引用
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优化:
- 跳过非引用字段:如
int、long等原生类型不扫描。 - 批量处理:连续 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/CMS | ZGC |
|---|---|---|
| 标记阶段 | STW 扫描全卡表(>10ms) | 并发卡清洗(无 STW) |
| 扫描粒度 | 512字节全页扫(80% 冗余) | 精确引用扫描(0 冗余) |
| 跨代引用存储 | 无持久化结构(每次全扫) | 全局 RSet(长效记录) |
| 可扩展性 | 堆越大 STW 越长 | 堆大小与停顿解耦(恒 <1ms) |
💎 设计价值:工程智慧的结晶
- 脏页标记(卡表):
- 最小化写屏障开销(5ns/次),避免实时维护复杂结构。
- 引用精炼(卡清洗):
- 并发执行:将 CPU 密集型任务移出 STW 关键路径。
- 按需精度:仅当卡页被标记时才启动扫描。
- 精确处理(RSet):
- 空间换时间:牺牲少量内存(0.01% 堆),换取 Minor GC 确定性低延迟。
最终结论:
ZGC 通过 “粗标记 → 精提取 → 高效扫” 的三级流水线,将跨代引用处理转化为 并发任务,使 Minor GC 停顿与堆大小彻底解耦。这是其实现 TB 堆亚毫秒级回收的核心基石,也是相比 ParNew/CMS 的代际突破。