专栏文章
专栏文章
GC 算法系列
1. GC 算法 #01:三色标记与写屏障 2. GC 算法 #02:分代 GC 3. GC 算法 #03:G1 GC(Region-based) 4. GC 算法 #04:ZGC(染色指针与负载屏障) 5. GC 算法 #05:引用计数与循环引用检测

GC 算法 #03:G1 GC(Region-based)

发布于 2026-06-02 02:40 👁 9 次阅读
#GC#JVM#算法#G1#engineering#region

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 regions

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)

暂无评论,来留下第一条吧。

发表评论