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

记忆集(Remembered Set,简称 RSet)和卡表(Card Table)是 JVM 在分代垃圾回收(尤其是 G1、CMS 等并发/增量式收集器)中用来精准跟踪跨代引用、避免全堆扫描的关键优化机制。它们不直接参与对象标记或回收,而是通过空间换时间,大幅减少 GC 时扫描的范围,从而加速回收过程。
为什么需要记忆集?——解决“跨代引用”带来的扫描爆炸
在分代 GC 中,年轻代频繁回收,但老年代对象可能持有对年轻代对象的引用(比如一个全局缓存 Map 存了新创建的对象)。如果每次 YGC 都要扫描整个老年代来检查是否有指向年轻代的引用,开销会随老年代增长而线性上升,完全不可接受。
记忆集就是为每个年轻代分区(如 G1 的 Region)或年轻代本身,单独维护一个“谁可能引用了我”的集合。它只记录老年代中哪些内存区域存在指向本区域的引用,而不是具体到每个对象。这样 YGC 时只需扫描这些被标记的区域,跳过大量无关内存。
卡表:记忆集的底层高效实现方式
记忆集本身是个逻辑概念,而卡表是它最常用、最轻量的物理实现。JVM 将老年代(有时也包括整个 Java 堆)划分为固定大小的小块,称为“卡页”(Card Page),典型大小为 512 字节。
每张“卡”对应一个字节(或位)的卡表项。当老年代中某处执行了写操作(比如 obj.field = youngObj),且该字段引用了年轻代对象时,JVM 的写屏障(Write Barrier)会立即计算出目标地址落在哪张卡上,并将对应卡表项标记为“脏”(dirty)。
- 卡表用极小空间(例如 1TB 堆仅需 ~2MB 卡表)记录粗粒度引用位置
- 写屏障保证“有引用就必标记”,无漏标(但可能误标——某卡被标脏,实际里面没跨代引用)
- YGC 时,只遍历所有“脏卡”对应的内存区域,从中扫描可能存在的跨代引用
记忆集与卡表如何协同工作?
以 G1 收集器为例:
- 每个 Region 都有一个独立的记忆集(RSet),本质是一组“脏卡地址 + 引用信息”的哈希表或数组
- 卡表负责快速识别哪些卡变脏;后台线程或 GC 线程会把脏卡中的引用抽出来,更新到对应目标 Region 的 RSet 中
- YGC 或混合 GC 时,直接读取目标 Region 的 RSet,就知道哪些老年代 Region 里有指向它的引用,无需扫描全老年代
- RSet 是“引用源”的索引,卡表是“脏卡发现器”,二者配合让跨代引用追踪既准确又高效
一个小例子帮你理解加速效果
假设老年代 4GB,按 512B 分卡 → 共约 800 万张卡,卡表仅需 ~8MB 内存。一次 YGC 前,只有约 0.1% 的卡被写屏障标脏(比如 1 万张),对应约 5MB 内存需扫描。若无此机制,就得扫描全部 4GB 老年代 —— 性能差距达 **800 倍**。
这个加速不是靠“更快地扫”,而是靠“几乎不扫不需要扫的地方”。卡表和记忆集共同构成了 JVM 垃圾回收中“局部性感知”和“增量更新”的基础设施。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











