G1回收器的工作机制
G1(Garbage First)是JVM中一款里程碑式的垃圾回收器,它在Java 9成为默认GC。它的设计目标是:在可控的停顿时间内,尽可能高地实现吞吐量,同时避免CMS的碎片问题和传统分代GC的大停顿。
一、G1的核心设计思想
G1与传统分代GC最大的区别是:不再物理划分新生代和老年代,而是将整个堆划分为多个大小相等的Region。
1. Region化堆内存
bash
传统分代堆:
+-------------------------------+---------------------+
| 新生代 | 老年代 |
| Eden | S0 | S1 | |
+-------------------------------+---------------------+
G1堆(逻辑上仍分代,但物理上混合):
+----+----+----+----+----+----+----+----+----+
| E | O | S | H | E | O | O | E | F |
+----+----+----+----+----+----+----+----+----+
| O | E | S | H | O | F | E | O | S |
+----+----+----+----+----+----+----+----+----+
E=Eden S=Survivor O=Old H=Humongous(大对象) F=Free(空闲)
-
Region大小 :通常为1MB-32MB之间(2的幂次方,通过
-XX:G1HeapRegionSize设定),整个堆通常划分为约2048个Region。 -
逻辑分代 :G1仍然区分年轻代和老年代,但它们是Region的动态集合,不是连续的内存块。
-
Humongous Region:大小超过Region容量50%的对象,存入专门的巨型Region,避免移动开销。
2. 停顿预测模型
G1的独特之处:它维护一个停顿预测模型,基于历史数据,可以预测在某个时间内(如200ms)能回收多少内存。
-
可控停顿 :通过
-XX:MaxGCPauseMillis设置期望最大停顿时间(默认200ms)。 -
动态调整:G1会动态调整年轻代大小、回收策略,尽量满足此目标。
二、G1的工作阶段
G1将GC分为两大类:年轻代GC 和混合GC。
1. 年轻代GC(Young GC)
触发条件:Eden区占满时触发。
工作流程:
-
标记根对象:从GC Roots出发,找到存活对象。
-
复制存活对象:将Eden和Survivor中的存活对象,复制到新的Survivor Region或晋升到Old Region。
-
回收原Region:一次性清空原来的Eden和Survivor Region,变为空闲状态。
特点:
-
使用复制算法,无碎片。
-
**STW(Stop The World)**发生,但停顿时间通常很短(因为存活对象少)。
-
完全并发的不多,但G1通过多线程并行复制来减少停顿。
bash
初始状态:
[E] [E] [E] [S] [O] [O] [F] [F]
Young GC后:
[F] [F] [F] [S'] [O+] [O] [F] [F]
↑
新Survivor/晋升
2. 并发标记周期(Concurrent Marking Cycle)
这是G1最复杂的部分,不产生STW或者极小STW地标记出老年代中的存活对象,为后续的混合GC做准备。
包含多个子阶段:
阶段1:初始标记(Initial Mark)- STW
-
标记所有从GC Roots直接可达的对象。
-
借机完成:通常在Young GC时顺带完成,代价很小。
阶段2:根区域扫描(Root Region Scan)
-
扫描Survivor Region中指向老年代的引用。
-
这个阶段是并发执行的(不暂停应用线程)。
阶段3:并发标记(Concurrent Mark)- 并发
-
从GC Roots开始,并发地遍历整个堆的对象图,标记所有存活对象。
-
这个过程对应用线程影响小,但可能产生浮动垃圾(标记过程中新产生的垃圾)。
阶段4:最终标记(Final Remark)- STW
-
STW,处理并发标记阶段遗漏的少量引用变更(通过SATB算法确保完整性)。
-
相比CMS的最终标记,G1的效率更高。
阶段5:清理(Cleanup)- STW
-
统计每个Region的存活对象数量和回收价值。
-
不实际回收,只是标记哪些Region可以完全回收(存活对象为0)。
-
根据停顿预测模型,按价值高低对Region排序,准备混合GC。
3. 混合GC(Mixed GC)
触发时机 :并发标记完成后,老年代占堆的比例达到阈值(-XX:InitiatingHeapOccupancyPercent,默认45%)。
核心特点 :不仅回收年轻代,还回收部分老年代Region。
工作流程:
-
选择Region集合(CSet):根据停顿预测模型,从高价值Region集合中,选出本次GC要回收的Region。包括:
-
所有年轻代Region
-
部分老年代Region(价值最高的那些)
-
-
复制存活对象:
-
将CSet中所有Region的存活对象,复制到其他空闲Region。
-
这相当于一次全局复制,对象可能从年轻代到年轻代、年轻代到老年代、老年代到老年代。
-
-
回收原Region:清空整个CSet,释放大量内存。
多次执行 :一次并发标记周期后,可能会触发多次混合GC,每次回收一部分高价值老年代Region,直到老年代内存占用低于阈值。
bash
并发标记后,按价值排序:
Region: O5(价值高) O3(价值高) O8(价值中) O2(价值低) ...
第一次混合GC:回收 O5, O3 + 所有年轻代Region
第二次混合GC:回收 O8 + 当前年轻代Region
...
4. Full GC
触发条件:
-
混合GC无法跟上对象分配速度(老年代增长太快)。
-
巨型对象分配失败(Humongous Region无法找到连续空间)。
-
并发标记周期失败。
后果 :最严重的STW,G1会退化到Serial Old算法,单线程进行全堆标记-整理-压缩。
目标:通过调优参数,尽量避免Full GC发生。
三、G1的关键技术点
1. SATB(Snapshot-At-The-Beginning)
G1在并发标记开始时,会对对象图拍一个"逻辑快照"。标记过程中,即使引用发生变化(比如删除了某个引用),G1仍然会保留该引用指向的对象为"存活"。
优点 :避免CMS中复杂的增量更新,实现简单。
代价 :可能产生浮动垃圾(本应回收但被保留的对象),需要后续GC处理。
2. Remembered Set(RSet)
每个Region内部维护一个RSet,记录哪些Region引用了本Region中的对象。
bash
Region A中的对象obj被Region B和Region C引用
Region A的RSet = [B, C]
作用:
-
Young GC时,不需要扫描整个堆,只需扫描RSet中记录的Region。
-
实现并行化 和分代独立回收。
维护开销:写屏障(Write Barrier)在引用赋值时更新RSet,略有性能损耗。
3. 停顿预测模型
G1维护一个历史数据库,记录每次GC的:
-
Region扫描时间
-
对象复制时间
-
更新RSet时间
基于这些数据,建立一个线性回归模型,预测回收N个Region需要的时间,从而在制定CSet时做到"停顿可控"。
四、G1 vs CMS vs Parallel GC
| 特性 | G1 | CMS | Parallel GC |
|---|---|---|---|
| 设计目标 | 可控停顿时间 | 低停顿 | 高吞吐量 |
| 堆布局 | Region化 | 连续分代 | 连续分代 |
| 内存碎片 | 无(复制+整理) | 有(标记-清除) | 无(整理) |
| 停顿时间 | 可预测(默认200ms) | 较低 | 较高(Full GC很长) |
| 吞吐量 | 中等 | 较低 | 最高 |
| 内存开销 | 较高(RSet占5%-10%) | 中等 | 低 |
| 适用场景 | 大堆(4GB+)、响应重要 | 中堆、响应重要 | 高吞吐、可接受停顿 |
选择建议:
-
G1:堆 > 4GB,期望停顿 < 1秒,Java 9+默认。
-
Parallel GC:堆 < 4GB,或吞吐优先(如批处理)。
-
ZGC:堆 > 32GB,期望停顿 < 10ms(Java 11+)。
五、G1调优示例
bash
# 设置期望最大停顿时间(G1会尽量满足)
-XX:MaxGCPauseMillis=200
# 设置堆大小和Region大小
-Xmx8g
-XX:G1HeapRegionSize=16m
# 触发混合GC的堆占用阈值(避免过早/过晚)
-XX:InitiatingHeapOccupancyPercent=45
# 并行线程数
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
# 启用G1日志
-Xlog:gc*:file=g1-gc.log:time,level,tags
调优方向:
-
停顿时间设置不当:太短(<50ms)可能频繁GC,太长(>500ms)可能失去G1意义。
-
Humongous对象问题:大量大对象会直接进入老年代,加速Full GC。可以通过调整Region大小或代码优化解决。
-
RSet内存占用:G1的RSet可能占堆内存5%-10%,避免过多跨Region引用。