G1:JDK9默认GC,面向大堆、可控STW停顿,核心思想:优先回收垃圾最多的Region,不再整代回收,增量式局部回收,复制算法做疏散,解决CMS内存碎片问题。
一、内存模型:Region分区架构
传统Parallel/CMS:堆是连续的新生代+老年代,物理隔离。
G1把整个Java堆切分成若干大小相等Region(1M‑32M,2的幂,默认总数约2048个),新生代、老年代不再物理连续,Region运行时动态扮演角色。
Region四种类型:
- Eden:新对象分配
- Survivor:YoungGC存活对象
- Old:晋升老年代对象
- 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占满。
流程:
- GC Root扫描 + 扫描RSet,标记存活对象
- 将Eden存活对象复制疏散到Survivor;Survivor对象年龄达标晋升Old Region
- 清空全部Eden Region,归还空闲
只回收Eden,不碰老年代;STW,多线程并行。
2. 并发标记周期(为MixedGC做准备,老年代占比达到IHOP阈值触发)
‑XX:InitiatingHeapOccupancyPercent=45,老年代占堆45%启动并发标记周期,不是FullGC,目的统计老年代每个Region存活占比,筛选高收益Region给MixedGC回收。
完整并发标记5阶段:
-
初始标记 Initial‑Mark(STW)
只标记GC Roots直接可达对象,依附在一次YoungGC末尾,STW很短。 -
Root Region Scanning 根分区扫描(并发)
扫描刚晋升到Survivor的对象作为根,必须在下一次YGC前完成,否则被YGC打断等待。 -
Concurrent Mark 并发标记(与业务线程并发执行)
遍历对象图标记存活,SATB快照写屏障算法(Snapshot‑At‑The‑Beginning)
CMS用增量更新,G1用SATB:标记开始瞬间对堆打快照;即使业务线程删掉引用,写屏障把旧引用压入SATB buffer,保证不会漏标存活对象;Remark只扫描SATB缓冲区,STW远短于CMS的Remark阶段。
-
Remark 重新标记(STW)
STW,处理SATB缓冲区、处理RSet变动,完成全部存活标记。 -
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)
触发场景:
- MixedGC回收速度跟不上分配速率;
- Evacuation Failure 疏散失败:复制对象找不到足够空闲Region;
- Humongous大对象找不到连续Region;
- 元空间耗尽。
JDK8 FullGC单线程标记‑整理,STW秒级,灾难;JDK10+改为多线程FullGC,但依然是全堆压缩,开销巨大,生产必须规避。
三、SATB vs CMS增量更新(面试高频)
| 项目 | G1 SATB | CMS增量更新 |
|---|---|---|
| 快照点 | 标记开始时刻快照 | 标记结束 |
| 漏标防护 | 写屏障保存删除的旧引用 | 写屏障记录新插入引用 |
| Remark工作量 | 只扫描SATB缓冲区,STW短 | 重新扫描全部老年代,STW长 |
| 浮动垃圾 | 多一些浮动垃圾 | 浮动垃圾少 |
SATB牺牲少量浮动垃圾,换取更短Remark停顿,更适合大堆低延迟场景。
浮动垃圾:并发标记阶段新产生对象,本次不回收,留到下一轮GC。
四、G1优缺点
✅优点
- 可预测停顿(软实时),
MaxGCPauseMillis目标控制STW,适合4‑32GB大堆服务; - 复制疏散算法,Region整体释放,解决CMS老年代内存碎片;
- 增量回收,不需要每次扫描整堆;
- JDK9+官方默认GC。
❌缺点
- RSet、写屏障带来CPU+内存开销(额外堆内存5‑20%);
- 堆小于4GB,吞吐量不如ParallelGC,不适合小堆;
- Humongous巨型对象坑多,频繁分配大对象性能暴跌;
- 参数多,调优门槛高于ParallelGC;
- 依然会发生FullGC,一旦发生代价高。
五、生产高频坑
-
EvacuationFailure疏散失败 → 退化成FullGC
原因:MixedGC复制存活对象时没有足够to‑space;
优化:调大堆、降低IHOP更早启动并发标记,调大G1ReservePercent(默认10)预留内存。 -
Humongous大对象泛滥
超过Region一半就是巨型对象,尽量避免短期大量生命周期短的大对象。
-
RSet开销过高
大量跨Region引用,写屏障CPU占用高;避免大量跨代循环引用。 -
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开销更高。