rset不是卡表翻版,而是每个region的反向引用哈希表(point-in),其膨胀源于跨region引用密度高、region大小不当及写屏障频繁触发;需通过jstat观察rs列、mixed gc日志中的rset扫描耗时定位瓶颈。

直接看 GC 日志里 RSet 相关字段,就能判断跨 Region 引用是否成了瓶颈;不是卡表太大拖慢 GC,而是 RSet 条目过多、更新太密、Region 太小或太大导致写屏障和扫描开销失控。
为什么 RSet 不是“卡表”的简单翻版
G1 已完全弃用传统全局 CardTable 结构。你看到的 RSet: 24576K, 128000 entries 日志,本质是某个 Region 的反向引用哈希表——它存的是“谁引用了我”,不是“我在哪张卡里被标记”。CMS 是 point-out(记录我指向谁),G1 是 point-in(记录谁指向我),所以膨胀只跟反向引用密度有关,跟本 Region 对象数量无关。
常见错误现象:
- 误把
RSet当成可调优的缓存,试图通过增大堆外内存解决 —— 实际上 RSet 内存本身就在堆外,扩内存不解决哈希冲突和写屏障延迟 - 在 Young GC 日志里紧盯
RSet大小 —— 真正压力在 Mixed GC 阶段的 RSet 扫描耗时,Young GC 只读不写 RSet
RSet 膨胀的真实诱因:写屏障 + 引用模式
每次老年代对象修改对年轻代/其他老年代对象的引用(如 oldObj.field = youngObj),都会触发一次 post-write barrier,进而更新目标 Region 的 RSet。这个过程不是免费的:
- 写屏障本身有 CPU 开销,尤其在高吞吐写场景下(如缓存批量更新、图遍历赋值)
- RSet 底层是哈希表,
entries超过一定阈值后,插入/查询从 O(1) 退化为 O(n) - 若业务存在强网状引用(比如一个
CacheNode被数百个IndexRegion同时引用),单个 Region 的 RSet 可能突破 10 万条目
实操建议:用 jstat -gc <pid></pid> 观察 RS 列(Remembered Set memory usage),若长期 >15% 堆大小,说明 RSet 元数据已吃掉大量堆外内存,不是 GC 策略问题,而是引用模型需要收敛。
-XX:G1HeapRegionSize 怎么选才不影响 RSet 效率
Region 大小直接决定 RSet 的粒度与哈希表负载:
- Region 太小(如 512KB)→ Region 数量爆炸 → RSet 实例数暴涨 → 堆外内存碎片化严重,
jcmd <pid> VM.native_memory summary</pid>中Internal类别会异常升高 - Region 太大(如 32MB)→ 单个 RSet 哈希桶易堆积数万条目 → 插入延迟上升;且容易触发 Humongous 分配,绕过 RSet,导致跨代引用漏检
- 推荐起点:堆 ≥32GB 时,显式设为
-XX:G1HeapRegionSize=8M;若应用引用关系密集(如实时风控图谱),可试4M换取更细粒度隔离
注意:-XX:G1RSetUpdatingPauseTimePercent=10 可限制 RSet 更新线程在 GC 暂停中占用的比例,但治标不治本 —— 它只是把写屏障延迟摊薄,不能减少总条目数。
怎么定位高开销的 RSet Region
不能靠猜,必须交叉验证:
- 启用
-XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy,重点抓 Mixed GC 日志中[RSet scanning: 12.4ms]这类耗时项,而非[RSet: xxxK] - 配合
jstat -gc <pid> 1000 5</pid>看RS和EC(Eden Capacity)变化趋势:若 RS 持续上涨而 EC 波动不大,说明老年代到老年代的跨 Region 引用在积累 - 用
jmap -histo:live <pid></pid>查看高频被引用的对象类型,再结合代码确认是否真有必要维持如此密集的反向引用
真正难处理的从来不是 RSet 本身,而是业务层把“谁引用我”当成了默认设计习惯 —— 比如用 Map











