G1(Garbage-First)是 JDK 9+ 的默认 GC,目标是在可预测的暂停时间内完成垃圾回收。核心创新是将堆划分为大小相等的 Region,打破了分代 GC 的固定边界,按"垃圾最多优先"原则动态选择回收集合。本文深入 Region 管理、RSet、SATB 写屏障和 Mixed GC 的算法细节。
相关文章:三色标记与写屏障 · 分代 GC · ZGC(染色指针与负载屏障)
目录
| 章节 | 说明 |
|---|---|
| 为什么需要 G1 | Parallel GC 和 CMS 的局限 |
| Region 机制 | 堆的分割方式与 Region 类型 |
| Remembered Set(RSet) | 跨 Region 引用的追踪 |
| 四个 GC 阶段 | Young GC / 并发标记 / Mixed GC / Full GC |
| 完整伪代码 | Young GC + Mixed GC 的核心逻辑 |
| 暂停时间预测模型 | 如何选择回收集合 |
| 执行追踪 | 一次 Mixed GC 的完整过程 |
| 异常场景 | Humongous 对象 / Evacuation Failure / Full GC |
| G1 vs CMS vs ZGC | 选型参考 |
为什么需要 G1
Parallel GC(JDK 8 默认):
吞吐量好,但 Full GC 暂停时间无法控制
大堆(16 GB+)Full GC 可能停顿几十秒
CMS(Concurrent Mark-Sweep):
并发标记减少暂停,但有两大缺陷:
1. 内存碎片:标记-清除不压缩,最终碎片化导致 Concurrent Mode Failure
2. 无法预测暂停:Remark 阶段暂停时间随堆大小增长
G1 的设计目标:
1. 可预测暂停:-XX:MaxGCPauseMillis(默认 200ms)
2. 不产生碎片:复制算法压缩内存
3. 高吞吐:并发标记,尽量减少 STW
4. 大堆友好:几 GB 到几十 GB 均适用
Region 机制
G1 将堆均匀切分为 2048 个 Region(可调),每个 Region 大小相同:
Region 大小 = 堆大小 / 2048(取最接近的 2 的幂次,1~32MB)
例:16GB 堆 → Region = 16GB/2048 = 8MB
Region 类型(动态分配,随 GC 周期变化):
Free:未使用,可分配为任意类型
Eden:当前 Young GC 的分配区
Survivor:Young GC 后存活对象的暂存区
Old:长命对象,经 Young GC 晋升而来
Humongous:单个对象 >= 1/2 Region 大小,直接分配,连续多个 Region
不同于传统分代 GC:
没有固定的 Young/Old 边界
Young 区可以动态扩缩(G1 根据暂停时间目标自动调整 Young Region 数量)
Remembered Set(RSet)
问题:Young GC 时,老年代可能引用年轻代对象,需要找到这些跨 Region 引用。
G1 的解法:每个 Region 有自己的 Remembered Set(RSet)
RSet 记录"哪些其他 Region 的哪些 Card 引用了本 Region 的对象"
即:RSet[R] = {(R', card_idx) | R' 有字段指向 R 中的对象}
Card Table(全局):堆按 512B 分块,每块一个 dirty byte
写屏障触发:修改引用时,将源 Card 标为 dirty
后台线程 Refinement Threads:
定期扫描 dirty Card Table
将 dirty Card 登记到目标 Region 的 RSet 中
清除 dirty 标记
Young GC 时:
只扫描 Eden + Survivor Region 及其 RSet 中的老年代引用
不需要扫描整个老年代 ✅
RSet 的代价:
存储开销:每个 Region 的 RSet 需要内存存储
平均约占堆的 5~10%(取决于跨 Region 引用密度)
更新开销:每次写引用都可能触发写屏障 → dirty Card → Refinement 更新 RSet
Refinement Threads 数量可配置:-XX:G1ConcRefinementThreads
四个 GC 阶段
阶段一:Young GC(STW,每次分配一定量后触发)
回收所有 Eden Region 和 Survivor Region
存活对象复制到新的 Survivor 或 Old Region
Eden/Survivor Region 变回 Free
暂停时间目标:
G1 根据 -XX:MaxGCPauseMillis 动态调整 Eden Region 数量
Eden 越少 → Young GC 处理的对象越少 → 暂停越短
代价:GC 频率增加(Eden 小则更快满)
阶段二:并发标记(大部分并发)
触发条件:Old Region 占堆比例 > -XX:InitiatingHeapOccupancyPercent(默认 45%)
子阶段:
2a. Initial Mark(STW,极短):标记 GC Roots,捎带 Young GC 执行
2b. Root Region Scan(并发):扫描 Survivor Region 引用的 Old 对象
2c. Concurrent Mark(并发):SATB 三色标记遍历所有存活对象
2d. Remark(STW,短):处理 SATB 缓冲区残余,完成标记
2e. Cleanup(STW,短):
统计每个 Region 的存活率(dead ratio)
完全空的 Region 立即回收加入 Free List
存活率低的 Region 标记为 Mixed GC 候选
阶段三:Mixed GC(STW,多次小批次)
回收所有 Young Region + 部分 Old Region(垃圾最多的若干个)
Old Region 选择标准:
按 "dead ratio"(垃圾比例)降序排列
选择死亡率最高的 Region,直到回收量满足预期或达到暂停时间上限
"Garbage First" 名称来源:
优先选择垃圾最多(dead ratio 最高)的 Region 回收
单位时间内回收的内存最多 ← 这就是 Garbage-First 的含义
Mixed GC 通常连续触发 8 次(-XX:G1MixedGCCountTarget),
每次回收一批 Old Region,分摊暂停时间
阶段四:Full GC(STW,最后手段)
触发条件:
Mixed GC 速度赶不上对象晋升速度(Evacuation Failure)
Humongous 对象分配失败
Full GC 使用串行标记-压缩(Serial Mark-Compact),暂停时间长
应尽力避免:调大堆、调低 InitiatingHeapOccupancyPercent 提早触发并发标记
完整伪代码
Young GC
def young_gc():
STW_begin()
# 收集根 + RSet 中的跨 Region 引用
roots = scan_gc_roots() + scan_rsets(young_regions)
to_space = select_free_regions(count=survivor_target)
for root in roots:
if root.ref in eden_regions or root.ref in survivor_regions:
evacuate(root.ref, to_space)
# 清空 Eden 和旧 Survivor
for r in eden_regions + old_survivor_regions:
r.reset_to_free()
# 更新 Young Region 集合
eden_regions = []
survivor_regions = to_space.survivor_regions
old_regions += to_space.promoted_regions
STW_end()
adjust_eden_count_for_next_gc() # 根据实际暂停时间调整
evacuate(obj, to_space)
def evacuate(obj, to_space):
if obj.is_forwarded():
return obj.forwarding_ptr # 已被移动
dest = allocate_in_to_space(to_space, obj.size())
if dest is None:
# 目标空间不足 → Evacuation Failure → 触发 Full GC
raise EvacuationFailure()
copy(obj, dest)
obj.forwarding_ptr = dest # 转发指针
dest.age = obj.age + 1
if dest.age >= tenuring_threshold:
mark_region_as_old(dest)
else:
mark_region_as_survivor(dest)
# 递归疏散子对象
for child in dest.fields:
if child in young_regions:
new_child = evacuate(child, to_space)
dest.update_field(child, new_child)
return dest
暂停时间预测模型
G1 内置基于历史数据的预测模型(衰减平均,Decay Average):
对于每个 Old Region,G1 预测:
扫描时间 = predict_scan_time(region.live_bytes)
复制时间 = predict_copy_time(region.live_bytes)
总时间 = 扫描时间 + 复制时间
Mixed GC 时,贪心选择 Region:
按 reclaimable_bytes / estimated_time 降序排列
依次选取,直到 predicted_total_pause >= MaxGCPauseMillis * 0.8
伪代码:
def select_cset_for_mixed_gc():
cset = all_young_regions # 所有 Young Region 必选
remaining = max_pause - predict_young_time()
candidates = sorted(old_candidates,
key=lambda r: r.reclaimable / r.scan_time, reverse=True)
for region in candidates:
if predict_time(region) > remaining:
break
cset.append(region)
remaining -= predict_time(region)
return cset
执行追踪
16GB 堆,Region=8MB,MaxGCPauseMillis=200ms:
T=0: Eden 区 256 个 Region(2GB)分配满
触发 Young GC(STW ~100ms)
存活对象 → 32 个 Survivor Region
晋升 → 16 个 Old Region(累计 Old=200 Region)
T=10s: Old 占堆 45%(200×8MB / 16GB)= 触发并发标记
Initial Mark(STW ~5ms,捎带 Young GC)
Concurrent Mark(并发,~500ms)
Remark(STW ~20ms)
Cleanup(STW ~10ms)
标记出 Old Region 死亡率:最高 90%,最低 5%
T=11s: Mixed GC #1(STW ~180ms)
回收全部 Young + 死亡率前 20 个 Old Region(节省 140MB)
T=11.2s: Mixed GC #2...#8(每次 ~150ms)
每次回收若干 Old Region,共释放约 1GB Old 空间
T=12.5s: Mixed GC 完成,Old 占堆降至 35%
恢复正常 Young GC 循环
异常场景
Humongous 对象的特殊处理
Humongous 对象(>= Region 大小/2,如 > 4MB):
直接分配到连续的 Old Region(Humongous Region)
Young GC 不会移动它(复制开销太大)
只有并发标记阶段才能回收
问题:
大量 Humongous 对象 → 老年代碎片化 → 分配失败
解法:
1. 调大 Region 大小(-XX:G1HeapRegionSize)
2. 避免创建大量大对象(用对象池复用)
3. JDK 8u60+ 支持 Humongous 对象在 Young GC 中回收
Evacuation Failure(疏散失败)
原因:Mixed GC 时没有足够的空闲 Region 容纳存活对象
触发后的处理:
1. 停止当次 Mixed GC 的疏散
2. 已成功疏散的对象保留,未疏散的对象原地保留(不移动)
3. 触发 Full GC(串行 Mark-Compact)修复碎片
预防:
增大堆(留更多 Free Region 缓冲)
降低 InitiatingHeapOccupancyPercent(更早触发并发标记)
减少大对象分配
G1 vs CMS vs ZGC
| 维度 | CMS | G1 | ZGC |
|---|---|---|---|
| 最大暂停 | 几十~几百 ms | 可控(200ms 目标) | < 10ms |
| 吞吐量 | 中 | 中高 | 略低 |
| 内存碎片 | 有(不压缩) | 无(复制压缩) | 无(并发压缩) |
| 元数据开销 | Card Table | Card Table + RSet | Colored Pointers |
| 适用堆大小 | < 8GB | 4~64GB | > 16GB |
| JDK 版本 | 废弃(14) | 9+ 默认 | 15 正式,11+ 试验 |
参考资料
- Detlefs, D. et al. (2004). Garbage-First Garbage Collection
- Oracle G1 GC 调优指南:https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector.html
- 《深入理解 Java 虚拟机》第 3 章
评论 (0)
发表评论