卡表与记忆集(rset)是“粗筛+精筛”协作关系:卡表以512字节卡页为单位标记脏卡,快速定位可能含跨代引用的内存块;rset则记录具体引用来源region和卡索引,精准构建gc roots,二者协同使minor gc跳过99%老年代空间。

卡表和记忆集不是两种并列技术,而是“粗筛+精筛”的协作关系:卡表快速定位可能含跨代引用的内存块,记忆集(RSet)进一步明确这些引用属于哪个区域、该加入哪次GC的根集合。它们共同让Minor GC完全跳过99%的老年代空间。
卡表负责“扫哪几块”
HotSpot把老年代按512字节切分为卡页,每页对应卡表中一个字节。只要某卡页内有对象字段指向年轻代,该字节就被写屏障设为1(脏卡)。Minor GC时,JVM只遍历卡表中值为1的索引,定位到对应卡页,再扫描这些页里的对象——不查干净页,也不扫整块老年代。
- 卡页大小固定为512字节,是硬件寻址效率与精度的平衡点
- 用byte数组而非位图,避免位运算开销,提升写屏障响应速度
- 写屏障在每次跨代赋值(如oldObj.field = youngObj)时立即触发,确保卡表实时准确
RSet负责“这些块属于谁”
卡表只告诉GC“第X张卡脏了”,但没说这张卡属于哪个Region、该加到哪个收集集的根里。RSet补上这一环:每个Region独立维护一张RSet,本质是哈希表+位图,记录“哪些Region的哪些卡页引用了我”。Young GC时,直接读取Eden/Survivor的RSet,就能精准拉入相关老年代对象作为GC Roots。
- G1中RSet不存具体对象地址,只存源Region起始地址 + 卡索引
- RSet膨胀取决于“被多少Region引用”,和本Region对象数量无关
- Mixed GC回收某个Old Region时,仅加载它的RSet,反向查源Region和脏卡,彻底避开无关内存
两者协同才能真正消除全堆扫描
单靠卡表会多扫——一张脏卡里可能只有1个跨代引用,却要遍历整页对象;单靠RSet无法落地——它需要卡表提供脏卡定位能力。实际流程是:写屏障标记脏卡 → Refinement线程异步解析脏卡,把真实跨代引用录入对应目标Region的RSet → GC时先查RSet确认来源Region,再按卡表索引聚焦扫描。
- 这种组合是保守近似:宁可多扫一两张卡,也绝不错漏一个引用
- 使Young GC停顿时间与老年代总大小解耦,哪怕堆64GB,只要跨代引用少,停顿仍可控
- 避免了CMS时代全局卡表随老年代线性膨胀的问题
验证机制是否生效的实用方法
不用看源码也能确认这套机制在跑:开启-Xlog:gc+refine=debug,Minor GC日志里出现Dirty card processing: 23 cards就说明卡表正在工作;用JFR录制后筛选CardTableUpdate事件,能直接看到跨代赋值触发写屏障的痕迹;jstat观察OGC变化的同时估算脏卡占比,长期高于5%提示跨代引用偏多,可能需调整对象生命周期或缓存策略。











