一文搞懂G1(Garbage‑First)底层原理

一文搞懂G1(Garbage‑First)底层原理

G1:JDK9默认GC,面向大堆、可控STW停顿,核心思想:优先回收垃圾最多的Region,不再整代回收,增量式局部回收,复制算法做疏散,解决CMS内存碎片问题。

一、内存模型:Region分区架构

传统Parallel/CMS:堆是连续的新生代+老年代,物理隔离。
G1把整个Java堆切分成若干大小相等Region(1M‑32M,2的幂,默认总数约2048个),新生代、老年代不再物理连续,Region运行时动态扮演角色。

Region四种类型:

  1. Eden:新对象分配
  2. Survivor:YoungGC存活对象
  3. Old:晋升老年代对象
  4. Humongous(巨型Region):对象 > 0.5*RegionSize,直接分配,占用1个或多个连续Region;大对象不会进Eden,默认仅FullGC回收,JDK8u40支持YGC回收死的巨型对象。

关键:回收单位是Region,不是整个新生代/老年代;G1统计每个Region的垃圾占比,优先回收收益最高的一批Region。

两个核心组件,解决分区带来的两大难题

1)Remembered Set(RSet) 记忆集【G1最核心】

问题:回收一个Region时,如果扫描全堆找外部对本Region的引用,代价巨大。

  • 每个Region自带1个RSet,记录其他Region指向本Region的跨Region引用(老→新生代、Old→Old),底层基于Card Table卡表(512字节为一张卡)记录脏卡信息。
  • YoungGC时,GC Roots = 普通GC Root + Eden/Survivor的RSet,不需要扫描全部老年代,STW时间不再随老年代变大而线性上涨。
  • 写屏障(Write Barrier):应用线程做对象引用赋值时,插入写屏障逻辑,更新卡表,异步更新RSet;带来5‑10%CPU开销。

RSet内存开销:堆的5%‑20%,堆越大开销绝对值越高。

2)CSet Collection Set 收集集合

单次GC(STW)要回收的Region集合。
G1根据MaxGCPauseMillis目标停顿时间,挑选一批高垃圾收益Region放入CSet,估算复制疏散耗时不超过目标停顿,做到可控STW。


二、G1的4种GC模式

1. Young GC(新生代回收,STW)

触发:Eden占满。
流程:

  1. GC Root扫描 + 扫描RSet,标记存活对象
  2. 将Eden存活对象复制疏散到Survivor;Survivor对象年龄达标晋升Old Region
  3. 清空全部Eden Region,归还空闲

只回收Eden,不碰老年代;STW,多线程并行。

2. 并发标记周期(为MixedGC做准备,老年代占比达到IHOP阈值触发)

‑XX:InitiatingHeapOccupancyPercent=45,老年代占堆45%启动并发标记周期,不是FullGC,目的统计老年代每个Region存活占比,筛选高收益Region给MixedGC回收。

完整并发标记5阶段:

  1. 初始标记 Initial‑Mark(STW)
    只标记GC Roots直接可达对象,依附在一次YoungGC末尾,STW很短。

  2. Root Region Scanning 根分区扫描(并发)
    扫描刚晋升到Survivor的对象作为根,必须在下一次YGC前完成,否则被YGC打断等待。

  3. Concurrent Mark 并发标记(与业务线程并发执行)
    遍历对象图标记存活,SATB快照写屏障算法(Snapshot‑At‑The‑Beginning)

CMS用增量更新,G1用SATB:标记开始瞬间对堆打快照;即使业务线程删掉引用,写屏障把旧引用压入SATB buffer,保证不会漏标存活对象;Remark只扫描SATB缓冲区,STW远短于CMS的Remark阶段。

  1. Remark 重新标记(STW)
    STW,处理SATB缓冲区、处理RSet变动,完成全部存活标记。

  2. Cleanup 清理阶段(部分STW)
    统计每个Old Region存活对象占比,按回收收益排序;直接释放完全无存活对象的Region;输出候选Region列表,准备MixedGC,不做对象复制

3. Mixed GC 混合GC(STW,G1特色)

并发标记周期结束后执行,不止回收新生代,同时回收一批高垃圾收益的老年代Region

  • CSet = 全部Eden + 部分Old候选Region
  • 将CSet内所有Region存活对象复制疏散到空闲Region,原Region直接释放。
  • MaxGCPauseMillis约束,一次MixedGC不会回收全部老年代候选Region;分多轮完成(默认最多8轮,G1MixedGCCountTarget),直到老年代占比降到安全水位,回到普通YoungGC模式。

MixedGC不是一次做完老年代清理,是分批做,以此控制单次停顿。

4. Full GC(尽量避免,全程STW)

触发场景:

  1. MixedGC回收速度跟不上分配速率;
  2. Evacuation Failure 疏散失败:复制对象找不到足够空闲Region;
  3. Humongous大对象找不到连续Region;
  4. 元空间耗尽。

JDK8 FullGC单线程标记‑整理,STW秒级,灾难;JDK10+改为多线程FullGC,但依然是全堆压缩,开销巨大,生产必须规避。


三、SATB vs CMS增量更新(面试高频)

项目 G1 SATB CMS增量更新
快照点 标记开始时刻快照 标记结束
漏标防护 写屏障保存删除的旧引用 写屏障记录新插入引用
Remark工作量 只扫描SATB缓冲区,STW短 重新扫描全部老年代,STW长
浮动垃圾 多一些浮动垃圾 浮动垃圾少

SATB牺牲少量浮动垃圾,换取更短Remark停顿,更适合大堆低延迟场景。

浮动垃圾:并发标记阶段新产生对象,本次不回收,留到下一轮GC。

四、G1优缺点

✅优点

  1. 可预测停顿(软实时)MaxGCPauseMillis目标控制STW,适合4‑32GB大堆服务;
  2. 复制疏散算法,Region整体释放,解决CMS老年代内存碎片
  3. 增量回收,不需要每次扫描整堆;
  4. JDK9+官方默认GC。

❌缺点

  1. RSet、写屏障带来CPU+内存开销(额外堆内存5‑20%);
  2. 堆小于4GB,吞吐量不如ParallelGC,不适合小堆;
  3. Humongous巨型对象坑多,频繁分配大对象性能暴跌;
  4. 参数多,调优门槛高于ParallelGC;
  5. 依然会发生FullGC,一旦发生代价高。

五、生产高频坑

  1. EvacuationFailure疏散失败 → 退化成FullGC
    原因:MixedGC复制存活对象时没有足够to‑space;
    优化:调大堆、降低IHOP更早启动并发标记,调大G1ReservePercent(默认10)预留内存。

  2. Humongous大对象泛滥

超过Region一半就是巨型对象,尽量避免短期大量生命周期短的大对象。

  1. RSet开销过高
    大量跨Region引用,写屏障CPU占用高;避免大量跨代循环引用。

  2. MixedGC回收老年代太少
    调小G1MixedGCLiveThresholdPercent(默认85),允许存活占比更高的Region进入回收候选集。

六、核心启动参数模板

‑XX:+UseG1GC
‑XX:MaxGCPauseMillis=200          # 目标停顿时间,不是硬保证
‑XX:InitiatingHeapOccupancyPercent=45
‑XX:G1ReservePercent=10
‑XX:ConcGCThreads=4               # 并发标记线程
‑XX:ParallelGCThreads=8           # STW并行GC线程
‑XX:+PrintGCDetails ‑Xlog:gc*

七、G1与CMS、ZGC简单对比

收集器 算法 停顿 适用堆 特点
ParallelGC 标记复制/整理 长STW,吞吐量优先 小‑中堆(<4G) 批处理离线任务
CMS 标记‑清除 低停顿,内存碎片 中等堆 JDK8主流,JDK14废弃
G1 分区复制疏散 可控STW,有碎片缓解 4‑32G 微服务、电商,JDK9默认
ZGC 染色指针,读屏障 亚毫秒级STW 大堆>16G 低延迟优先,JDK15+

G1定位:吞吐量和延迟之间做折中;ZGC进一步压低停顿,但CPU开销更高。