rset与card table是分层协作关系:card table以512字节为单位标记脏卡,提供粗粒度定位;rset作为每个region独立维护的哈希表+位图,记录“谁引用了我”,实现跨region反向索引;二者协同使gc仅扫描可能含引用的脏卡,避免全堆扫描。

G1 的 Remembered Set(RSet)和 Card Table 不是并列技术,而是分层协作关系:Card Table 是底层内存划分与粗粒度标记机制,RSet 是上层逻辑结构,专为跨 Region 引用设计,共同实现“不扫全堆、只查可能有引用的区域”。
Card Table 提供脏卡定位能力
Card Table 将整个堆按 512 字节切分为“卡页”(card),每张卡对应一个字节(非位图),用于标记是否被写入过跨 Region 引用。当老年代 Region 中的对象字段指向另一个 Region 的对象时,写屏障(post-write barrier)立即把源对象所在卡页标为“脏”。这样,GC 无需遍历整块 Region,只需聚焦于这些脏卡页。
- 卡页大小固定为 512 字节——兼顾硬件寻址效率与精度平衡
- 用 byte 数组而非 bit 数组,避免位运算开销,提升写屏障吞吐
- 脏卡本身不记录具体引用路径,只表示“这张卡里可能有跨 Region 引用”
RSet 实现反向引用索引
RSet 每个 Region 独立维护,本质是一个哈希表 + 卡页位图,存储的是“谁引用了我”,即记录哪些其他 Region 的哪些卡页中存在指向本 Region 的引用。它不存对象地址,只存 Region 起始地址 + 卡索引。
- Young GC 时,只读取 Eden/Survivor Region 的 RSet,就能快速获知哪些 Old Region 需加入 GC Roots
- Mixed GC 回收某个 Old Region 时,直接加载它的 RSet,反向定位到源 Region 和脏卡,跳过无关内存
- RSet 膨胀取决于“被多少 Region 引用”,和本 Region 对象数量无关
两者协同完成精准扫描
Card Table 解决“扫哪几张卡”,RSet 解决“这些卡属于哪个 Region、该加到谁的根集中”。例如:Region A 中某对象被 Region B 的第 37 张卡内对象引用,则写屏障触发后,会在 Region A 的 RSet 中插入一条记录(Key: Region B 起始地址,Value: 第 37 位为 1 的位图)。GC 扫描时先查 RSet 得到 Region B,再按位图找到第 37 卡,在该卡内精确检查字段引用。
- 这种组合是保守近似:可能多扫一两张卡,但绝不会漏掉真实引用
- 避免了传统 CMS 全局卡表随老年代扩大而线性膨胀的问题
- 使 Young GC 停顿时间与老年代总大小解耦,哪怕堆达 64GB,活跃跨区引用少,停顿仍可控
常见问题与调优关键点
RSet 性能瓶颈往往不是内存不够,而是结构失配或引用模式不合理:
- RegionSize 过小(如 512KB)→ Region 数量爆炸 → RSet 实例数暴涨 → 堆外元数据碎片化
- RegionSize 过大(如 32MB)→ 单个 RSet 哈希桶冲突严重 → 插入/查询延迟上升
- 高频更新缓存(如 ConcurrentHashMap)、强网状引用(如图谱节点被数百 Region 引用)→ RSet 条目激增、写屏障开销升高
- 观察手段:用 jstat -gc
看 RS 列;GC 日志中关注 [RSet: XMB, Y entries] 和 [scan: Zms] 是否持续超 20ms











