ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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
返回列表