JVM G1 回收器 RSet 与 PRT 深度解析

发布时间:2026/10/7 5:39:47

JVM G1 回收器 RSet 与 PRT 深度解析 源码版本OpenJDK 8u HotSpotsrc/share/vm/gc_implementation/g1/关键词G1、Remembered Set、Per Region Table、写屏障、Card Table、粗化Coarsening1. 背景为什么 G1 需要 RSetG1Garbage-First是一款以Region为回收基本单位、追求可预测停顿的低延迟收集器。它把堆切成大量大小相等的HeapRegion默认约 2048 个每个 1MB~32MB回收时不再像传统分代收集器那样整代扫描而是只挑选若干region组成Collection SetCSet进行增量回收。这就带来一个核心难题增量回收某个 region 时如何在不扫描整个堆的前提下找到谁还在引用我如果 A region 里的对象被 B、C、D 等多个 region 引用回收 A 时就必须扫描这些外部引用者否则会把仍被引用的对象误回收。但全堆扫描违背了 G1 局部回收的初衷。解决办法是Remembered Set记忆集合RSet每个 region 维护一个 RSet记录有哪些来自其他 region 的指针指向了我。回收时只要扫描本 region 的 RSet 指向的少量 card就能精确找到外部引用而不必遍历整个堆。G1 的 RSet 并不记录对象级引用而是记录到card卡片粒度——card 是堆上一段固定大小默认 512 字节的内存块由 Card Table 管理。ptr card_shift即可把一个对象地址映射到它所属的 card。2. 引用变化如何通知 RSet写屏障对象的引用发生变化赋值如果不被感知RSet 就会失效。G1 通过写屏障Write Barrier拦截每一次引用字段的写入G1RemSet::write_ref_field / par_write_ref └─ g1RemSet.inline.hpp:69-72 HeapRegion* to _g1-heap_region_containing(obj); if (from ! to) { to-rem_set()-add_reference(p, tid); // 把p 指向 obj记入 obj 所在 region 的 RSet }要点被引用对象obj所在的 region 是被指向方toRSet 记在 to 上。from ! to说明这是一次跨 region的引用才需要进 RSetregion 内部引用不影响增量回收无需记录。tid是触发写入的线程 id用于并发与 FromCardCache 去重。3. 类层级HeapRegion → HeapRegionRemSet → OtherRegionsTableHeapRegion (heapRegion.hpp:211 _rem_set; 491 rem_set()) └─ HeapRegionRemSet (heapRegionRemSet.hpp:229, 内含 OtherRegionsTable _other_regions) └─ OtherRegionsTable (heapRegionRemSet.hpp:120, RSet 主体即下文PRT 容器) ├─ _sparse_table (SparsePRT稀疏) ├─ _fine_grain_regions (PerRegionTable** 开放哈希表细粒度 —— 源码真正的 PRT) └─ _coarse_map (BitMap粗粒度)术语澄清容易读混常见资料把OtherRegionsTable称为 “PRTPer Region Table”。但在 Oracle 这份源码里注释heapRegionRemSet.hpp:100-104明确写道_fine_grain_entries array is an open hash table of PerRegionTables (PRTs)——也就是说源码中真正的 PRT PerRegionTable类而OtherRegionsTable是PRT 的集合/管理器。本文用源码口径PRT PerRegionTable。HeapRegionRemSet本身是个薄壳所有实际记录工作都转交给OtherRegionsTableheapRegionRemSet.hpp:307-314。4. OtherRegionsTable 的三级存储OtherRegionsTable要回答的唯一问题是“哪些来自其他 region 的card 含有指向我owner region的指针”它用三级粒度来回答本质是在记录精度与内存占用之间做权衡引用越稀疏越省空间引用越密集越追求精确。4.1 Coarse粗粒度——_coarse_map结构一整张BitMapheapRegionRemSet.hpp:128位数 整个堆的 region 总数。存储的值只有1 bit 一个 region 索引。第i位为 1 表示regioni可能包含指向我的指针不记录是哪个 card。写入命中即跳过if (_coarse_map.at(from_hrm_ind)) return;heapRegionRemSet.cpp:457。置位动作发生在 fine 表满、淘汰 PRT 时delete_region_table()把被淘汰 PRT 对应的 region 在_coarse_map中置 1cpp:622-625称为粗化coarsen。性能✅ 查询 O(1)一次 bit 测试内存极小N 个 region 只花 N bit是所有方案里最省空间的兜底。❌ 代价最大GC 扫描时因为不知道具体 card必须扫描整个引用者 region 的所有对象/card扫描成本最高。用_n_coarse_entries统计粗化数量。4.2 Fine细粒度—— PerRegionTable源码真正的 PRT结构_fine_grain_regions是PerRegionTable**数组heapRegionRemSet.hpp:132即开放哈希表 冲突链。长度_max_fine_entriescpp:284-286 2^log2(G1RSetRegionEntries)默认基数 256再乘region_size_log_mb1。哈希ind from_hrm_ind _mod_max_fine_entries_mask冲突用_collision_list_next串成链。每个PerRegionTable存什么heapRegionRemSet.cpp:41_hr引用者 region 指针_bmBitMap长度 CardsPerRegion一个 region 内的 card 总数每一位对应引用者 region 内的一个 card存储的值把该引用者 region 内哪些 card 含指向 owner 的指针编码进 bit 位。add_reference_workcpp:89把from转成相对该 regionbottom()的 card 偏移hw_offset (card_shift - LogHeapWordSize)再add_card_work置位_bm对应 bit并行用par_at_put 原子自增_occupied。写入/查询prt-add_reference(from)置位contains_reference直接return _bm.at(card_ind)。性能✅ 最精确GC 只扫被置位的少数 card而非整个 region扫描成本最低。内存每个 PRT 固定带一张CardsPerRegion位的 BitMap本身不便宜但数量有硬上限默认 256 量级总体有界。上限与淘汰粗化fine 表满_n_fine_entries _max_fine_entries触发delete_region_table()cpp:578在采样窗口_fine_eviction_stride/_fine_eviction_sample_size里挑选occupied 最大的 PRT把它对应的 region 粗化置 coarse bit并立即init复用该 PRT 给新 regioncpp:509。即牺牲精度换内存有界。查找无锁、改桶链才持_mPRT 通过静态_free_list对象池复用cpp:206-203避免频繁 new/delete。遍历高效_first_all_fine_prts/_last_all_fine_prts双向链表批量操作所有 fine PRT 时不必扫整个哈希表。4.3 Sparse稀疏——_sparse_tableSparsePRT为什么存在当一个引用者 region 只贡献了极少 card时为它建一张整 BitMap 的 PRT 太浪费先用紧凑结构表示。结构SparsePRT内含两张RSHashTable_cur/_next双缓冲sparsePRT.hpp:216-217。每张RSHashTable_buckets桶数组容量为 2 的幂用capacity_mask()取模哈希_entriesSparsePRTEntry连续数组变长对象桶内用SparsePRTEntry::_next_index串成冲突链。SparsePRTEntry存什么sparsePRT.hpp:45_region_ind引用者 region 索引_cards[1]变长数组存该 region 内、含指向 owner 指针的 card 偏移相对 region bottom最多cards_num()个cards_num()MAX2(G1RSetSparseRegionEntries ~(4-1), 4)sparsePRT.hpp:64默认基数G1RSetSparseRegionEntriesBase 4所以每个 entry 默认最多存 4 个 card且数量恒为 4 的倍数配合UnrollFactor 4循环展开。存储的值(引用者 region 索引 → 至多 4 个 card 偏移)的紧凑映射。写入/溢出add_card在 entry 的_cards里找空位放入sparsePRT.cpp:93带 4 路展开若 4 个槽都满 → 返回overflow→ 上层在OtherRegionsTable::add_reference走建/复用 PRT 把 sparse 卡迁过去分支heapRegionRemSet.cpp:528-541即sparse → fine 升级promote。性能✅ 最省内存只有真正有引用的 region 才占 entry且每个 entry 仅存几个 int 级 card 偏移而非整张 BitMap。哈希表初始容量仅 16InitialCapacitysparsePRT.hpp:222利用率 50%occupied_entries*2 capacity自动expand()sparsePRT.cpp:483。双缓冲_cur/_nextsparsePRT.hpp:213-217 注释GC 扫描迭代只读_cur新增/删除走_next从而支持unsynchronized reads/iterations并发读不被并发改破坏cleanup 阶段再 reconcile。循环展开UnrollFactor4加速_cards的查找与拷贝。❌ 代价查找需哈希 冲突链遍历比 coarse 慢单 entry card 上限小超过即升级到 fine用空间换精度。5. 升级路径与总体调度引用少 ── Sparse (每引用者 region 至多 4 个 card 偏移极省内存) │ 某 region 存满 4 个 card 再插第 5 个 → overflow ▼ 引用增多 ── Fine / PerRegionTable (整张 card 位图精确扫描最省) │ fine 表达上限 (_max_fine_entries默认 ~256) ▼ 过多引用者 ── Coarse (1 bit/region牺牲扫描精度保住内存上限)sparse → fine单 entry 4 个 card 溢出触发 promote并把已存的 sparse 卡迁入新建 PRT。fine → coarsefine 表满淘汰 occupied 最大的 PRT 并粗化cpp:578。整体策略是能省则省、逐级退让动态平衡记录开销与GC 扫描开销。6. 跨三级的通用加速FromCardCache 去重FromCardCacheheapRegionRemSet.hpp:50每线程 × 每 owner region 缓存最近处理的 card 索引是横跨三层的优化。add_reference一进来先查它FromCardCache::contains_or_replace(tid, cur_hrm_ind, from_card)cpp:444——同一线程、同一 owner region、同一 card 刚处理过就直接返回避免重复入表。这在高频赋值的写密集场景下显著减少重复工作。7. 并发与内存序OtherRegionsTable头部注释heapRegionRemSet.hpp:106-118明确其并发策略查找 PRT 可以无锁find_region_table但修改桶链必须持_mMutex。允许并发读到正在被删除/复用的 PRT这是安全的因为PRT 只会在安全点真正释放平时是被复用cpp:113删除 PRT 时会先置coarse_map的 bit所以漏记不了复用 PRT 时即使误加 bit 也无害cpp:116-118。add_reference_work用is_in_reserved_raw防御并发复用cpp:109新建 PRT 用OrderAccess::release_store_ptr发布cpp:525保证内容可见后才对外可用。8. 小结粒度存储结构存的值查询内存扫描精度Coarse1 bit/region_coarse_mapregion 索引可能存在O(1)极小必须扫整个引用者 regionFine / PRTPerRegionTable card 位图引用者 region 内具体 card 偏移O(1) 位测试中数量有上限只扫置位 card最精确SparseSparsePRT(region→≤4 card) 紧凑哈希region 索引 至多 4 个 card 偏移哈希链极小只扫记录的 cardG1 用这套能省则省、逐级退让的三级存储在记录成本维护 RSet 的写屏障开销、内存与GC 扫描成本回收时遍历 RSet 的开销之间取得了动态平衡这正是 RSet 能支撑 G1 增量、可预测停顿回收的关键设计。附关键源码位置速查内容文件:行HeapRegion::rem_set()heapRegion.hpp:491HeapRegionRemSet类heapRegionRemSet.hpp:229OtherRegionsTable类 / 三级字段heapRegionRemSet.hpp:120, 128/132/149add_reference主流程heapRegionRemSet.cpp:425delete_region_table粗化heapRegionRemSet.cpp:578PerRegionTable类 /add_reference_workheapRegionRemSet.cpp:41 / 89SparsePRT/SparsePRTEntrysparsePRT.hpp:210 / 45写屏障入口to-rem_set()-add_referenceg1RemSet.inline.hpp:72FromCardCache去重heapRegionRemSet.cpp:444
延伸阅读

更多相关文章

2026/10/2 17:47:11

RAG+Agent主程序架构设计:从核心原理到生产级实现

1. 从概念到现实:为什么RAGAgent是当前AI应用的核心范式如果你最近在关注AI应用开发,尤其是想构建一个能真正“理解”并“使用”你私有数据的智能助手,那么“RAGAgent”这个组合你一定绕不开。它不再是实验室里的概念,而是正在成为…

2026/10/5 14:44:44

XXL-Job双实例重复执行问题排查:从数据库主键失效到时钟同步陷阱

最近在排查一个线上任务重复执行的问题,客户反馈明明配置了单机串行执行,但日志里却出现了同一任务在同一时间被两个不同的执行器实例执行了两次。这直接导致了数据重复处理,下游系统报错。问题指向了分布式任务调度框架 XXL-Job,…

2026/10/5 6:49:00

AI前沿日报_2026-08-13

🤖 AI 前沿日报:2026年8月13日三大模型同日发布:Qwen 3.8开源2.4万亿参数MoE、Grok 4.6对标GPT-5.6、DeepSeek V4 Pro上线API;AI代理自主发现开源项目安全漏洞并获人类维护者合并;OpenAI伦理负责人入职不到一年辞职&am…

2026/10/7 6:35:22

CTF实战:ECDSA签名k值复用漏洞快速利用指南

1. 这不是密码学课,是夺旗现场的实战推演ECDSA签名漏洞——这六个字在CTF赛场上不是理论题,是倒计时开始后你盯着屏幕心跳加速的信号。我带过三届高校CTF战队,每年都有队员卡在Web或Crypto类题目里反复调试签名验证逻辑,最后发现根…

2026/10/7 6:35:22

WGAN生成动漫头像实战:TensorFlow实现与调参避坑指南

简介:这份资源是面向深度学习入门者与图像生成爱好者的Tensorflow WGAN动漫头像生成实战教程与配套源码,帮助读者从零理解生成对抗网络并动手训练出动漫风格头像。压缩包共23个文件,约122KB,以8个Python源码文件为核心&#xff0c…

2026/10/7 6:35:22

从GPS到UWB:Python仿真实现30厘米室内定位精度

1. 为什么我要从GPS转向UWB定位仿真做了三年室外定位项目,GPS模块换了一茬又一茬,从普通的单频模块到RTK差分,精度始终卡在米级下不去。室外开阔环境还好,一进室内或者楼宇密集区,GPS信号漂移得亲妈都不认识。后来接了…

2026/10/7 6:35:22

SIwave串扰仿真全流程:从Allegro导入到结果解读

1. 串扰分析到底在解决什么问题做硬件这行的朋友大概率都遇到过这种情况:板子打样回来,功能测试一切正常,但一跑高速信号就出问题——眼图塌了、误码率飙升、时序余量莫名其妙少了一大截。排查半天电源没问题、阻抗也控制得不错,最…

2026/10/7 6:35:22

TL072带通滤波器设计全解析:从原理计算到实测调试

1. 别急着查手册:先搞懂TL072在带通滤波器里到底扮演什么角色很多人在设计带通滤波器时,第一反应是翻开教材、找到那个著名的多重反馈带通滤波器公式,然后开始往里面填电容电阻值。公式确实没错,但如果你对TL072这款运放本身的脾气…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/6 17:46:51

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

SAP HANA查询结果导出CSV:避开乱码、性能与权限的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

数字后端Placement阶段Density与Congestion控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑