跨代引用通过卡表标记物理位置,RSet是其逻辑汇总;卡表每字节映射512B老年代内存,index=addr>>9,仅标脏/净;RSet按Region或代组织,异步扫描脏卡构建,Minor GC仅扫描脏卡及对应RSet,大幅降低扫描量。

直接说清楚:跨代引用本身不“存”在老年代某处,而是通过卡表标记其所在的物理位置;记忆集(RSet)是逻辑汇总,卡表是底层定位器——两者配合,让 Minor GC 只扫几KB脏卡,而非几GB老年代。
卡表的物理分布:512字节一卡,地址映射靠位运算
HotSpot 中卡表是一个紧凑的字节数组,每个字节对应老年代中一块 512 字节的内存区域(即“卡页”)。这个划分是固定且对齐的:
- 给定对象地址 addr,它所属卡页索引直接算: index = addr >> 9(右移 9 位,等价于除以 512)
- 卡表本身不存引用地址,只存状态:0 表示“干净”(暂无跨代引用),1 表示“脏”(该卡页内至少有一个对象引用了年轻代)
- 假设老年代占 2GB,则卡表仅需约 2GB / 512B ≈ 4MB 内存,空间开销极低
记忆集(RSet)不是卡表的复制,而是它的“精炼结果”
RSet 是按 Region(G1)或按代(CMS/Parallel)组织的引用关系索引表,它不直接映射内存地址,而是回答一个问题:“谁指向我?”
- G1 中,每个 Region 都维护自己的 RSet,内容是“哪些其他 Region 的哪些卡页里有指向本 Region 的引用”
- RSet 数据来自异步线程(G1ConcRefinementThreads)对脏卡页的精确扫描——先查卡表找出脏卡,再遍历卡页内所有对象,检查字段是否指向本 Region
- 所以 RSet 是延迟构建、按需加载的;它比卡表重得多(可能占堆内存 5%~20%),但换来的是 GC 根集合的精准性
Minor GC 实际扫描路径:从卡表出发,收口到 RSet
一次 Minor GC 不会碰老年代原始对象,只依赖两级结构协同工作:
- 第一步:遍历卡表,快速筛选出所有值为 1 的索引(即脏卡)——这步是 O(1) 查表,毫秒级
- 第二步:对每个脏卡,定位其在老年代的起始地址,扫描该 512 字节内存块内的所有对象头和引用字段
- 第三步:若使用 G1,还会查对应 Region 的 RSet,把其中记录的跨 Region 引用对象也加入 GC Roots;CMS 则跳过这步,直接把整个脏卡页对象都当作潜在根
- 最终真正被扫描的老年代内存,通常只有几十 KB 到几百 KB,远低于全老年代扫描的 GB 级开销
扫描开销的关键瓶颈不在卡表,而在写屏障与 Refine 线程失衡
卡表更新快、空间小,真正的性能风险藏在“脏卡生产”和“RSet 消费”的节奏差里:
- 每次老年代对象修改字段(如 cache.put(key, youngObj)),写屏障立即标记对应卡为脏——高频写入 → 卡表迅速变脏
- 但 RSet 构建是后台异步的;若 CCSU(Concurrent RS Update)时间占比持续超 10%,说明 Refine 线程跟不上,脏卡堆积
- Mixed GC 启动时若 RSet 还没刷完,就会漏标存活对象,触发 Concurrent Mode Failure 或被迫扩大根集合,Evacuation 时间飙升










