RSet比传统卡表更高效,因其将全局大表拆分为每个Region独立的小RSet,仅扫描有跨Region引用的源Region卡页,避免全堆扫描;RSet本质是哈希表+卡页位图,记录“谁引用我”,膨胀取决于被引用Region数而非本Region对象数。

G1 不靠全局卡表,而是为每个 Region 配一个 Remembered Set(RSet),专门记录“谁引用了我”。这解决了跨 Region 引用带来的扫描爆炸问题——不用每次 GC 都扫全堆,只查 RSet 列出的源 Region 卡页即可。
为什么 RSet 比传统卡表更高效
传统 CMS 的 Card Table 是一张全局数组,按 512 字节分卡,所有跨代引用都记在一张表里。一旦老年代变大,这张表就巨长,扫描开销线性增长。G1 把这张“大表”拆成 N 个小本子,每个 Region 自带一本 RSet,只登记指向它的跨 Region 引用。混合回收时,只需加载待回收 Region 的 RSet,定位到那几个“可能有引用”的源 Region,再精确扫描其中的卡页,跳过大量无关内存。
RSet 的真实存储结构与触发机制
RSet 本质是哈希表 + 卡页索引位图。它不存具体对象地址,只记“Region A 的第 X 张卡里,有对象指向我”。每次发生跨 Region 赋值(如 oldObj.field = youngObj),JVM 触发 write barrier,在目标 Region(youngObj 所在 Region)的 RSet 中插入一条反向引用记录。注意:RSet 膨胀只取决于“被多少 Region 引用”,和自己 Region 里有多少对象完全无关。
什么情况会让 RSet 成为性能瓶颈
- RSet 内存占用超过堆的 1.5%,或 GC 日志中 [RSet: XMB, Y entries] 后紧跟 [scan: Zms] >20ms
- 缓存类应用高频更新 Map、ConcurrentHashMap 或 PooledByteBuf,导致大量跨 Region Value 被反复写入
- RegionSize 过小(如 512KB),Region 总数暴涨 → RSet 实例数翻倍 → 元数据吃掉大量堆外内存
- RegionSize 过大(如 32MB),单个 RSet 哈希桶冲突严重,插入/查询延迟上升
调优 RSet 的三个实操要点
- RegionSize 匹配业务块大小:电商缓存常用 4MB 商品快照,就设 -XX:G1HeapRegionSize=4M;若常见缓冲区为 6–8MB,选 8M(必须是 2 的幂)
- 收拢写入路径:避免缓存 Value 分散在多个 Region;用对象池复用缓冲区,或把关联数据打包进同一 Region 分配
- 控制 Mixed GC 选区逻辑:冷热数据混在同一个 Old Region 会导致 G1 优先回收“垃圾多但业务已弃用”的区域;可加 cacheTTL 字段辅助识别生命周期,或调整 -XX:G1MixedGCCountTarget 和 -XX:G1OldCSetRegionThresholdPercent


















