parallel收集器中卡表是跨代引用的唯一机制,直接标记dirty卡页并扫描;g1中卡表仅作脏页探测器,服务于region级remembered set构建,实现异步refinement与混合回收。

卡表在 Parallel 收集器中:简单直接的 dirty 标记
Parallel Scavenge(新生代)搭配 ParallelOld(老年代)时,卡表是跨代引用处理的唯一支撑机制。它采用最基础但高效的实现:
- 卡页大小固定为 512 字节,不可配置,每个卡页对应 CardTable 数组中一个 byte 元素
- 写屏障仅在“老年代对象字段赋值指向年轻代对象”时触发,将对应卡索引设为非零值(如 0xff),其余写操作(如 young→old、内部引用)完全忽略
- Minor GC 前,扫描所有 dirty 卡页内的对象,逐字段检查是否真实引用了年轻代——不缓存、不聚合、不区分目标区域,结果直接作为 GC Roots 的一部分
- 没有记忆集(Remembered Set)结构;卡表就是全部,不构建 RSet,也不做异步 refinement
这种设计追求吞吐量最大化:写屏障开销极低,卡表遍历快,适合 CPU 密集、暂停时间可接受的批处理场景。但代价是每次 Minor GC 都要重复解析脏卡,且无法支持部分回收或分区粒度控制。
G1 中的卡表:只是底层基础设施,真正起作用的是 RSet
G1 不把卡表当作最终引用记录工具,而是将其降级为“脏页探测器”,服务于更精细的 Remembered Set 构建:
- 同样使用 512 字节卡页和写屏障标记 dirty 卡,但 dirty 卡不会被 GC 线程直接扫描;而是先入队到 Dirty Card Queue,由后台 Refinement 线程异步处理
- Refinement 线程从队列取卡页,扫描其中对象,用 OopMap 快速识别跨代引用,并按目标 Region 归类登记——例如 oldObj.field → EdenRegion_A,则在 EdenRegion_A 的 RSet 中添加一条记录(源 Region + 卡索引范围)
- 每个 Region 都有独立 RSet,结构是哈希表:key 是源 Region 起始地址,value 是该 Region 内涉及的 dirty 卡索引集合
- Young GC 时,只读取当前待回收 Region(如 Eden + Survivor)的 RSet,直接获得所有跨代引用源对象位置,无需再遍历卡页内容
这意味着 G1 的卡表不参与 GC 实时决策,只负责“线索采集”;RSet 才是真正的“引用目录”。这种分离让 G1 支持并发 refinement、可预测暂停、混合收集(Mixed GC),但也带来更高内存占用与写屏障延迟。
关键差异总结:目的、粒度与协作方式不同
两者都用卡表,但角色完全不同:
- 目的差异:Parallel 的卡表 = 引用定位器(直接用于 GC Roots 扩展);G1 的卡表 = 脏页信号灯(只为驱动 RSet 构建)
- 粒度差异:Parallel 按卡页(512B)粗筛后全量扫描对象;G1 按 Region(默认 1~32MB)组织 RSet,引用关系精确到 Region 级别,甚至可进一步细化到卡索引
- 协作差异:Parallel 无异步组件,卡表更新与 GC 强耦合;G1 引入 Dirty Card Queue 和 Refinement 线程,实现写屏障与 GC 的解耦,降低单次写操作延迟
- 扩展性差异:Parallel 卡表无法支撑并发标记或增量回收;G1 的 RSet + 卡表组合天然适配并发、并行、混合等多阶段回收模型
本质上,Parallel 把卡表用到了极致的“够用即止”,而 G1 把卡表用作了更大系统的一块基石。










