随着Java应用堆内存不断增大,传统垃圾回收器(如CMS)在内存碎片、大堆回收延迟等问题上逐渐力不从心。G1(Garbage First)垃圾回收器应运而生,以低延迟、大堆友好、无内存碎片为核心优势,在JDK9后成为默认回收器,彻底替代了CMS。本文将从基础认知、回收阶段、核心机制、版本演进四个维度,全面拆解G1的底层原理与实践细节,帮助开发者彻底掌握G1。
一、G1垃圾回收器基础认知
1.1 核心定义
G1(Garbage First),即"优先回收垃圾价值最高的区域",是一款面向大堆内存、低延迟场景 的垃圾回收器。它打破了传统分代回收的物理分区,将堆划分为多个大小相等的Region(区域),通过标记-整理算法 避免内存碎片,Region之间采用复制算法回收存活对象,核心目标是将GC暂停时间控制在用户指定的范围内(默认200ms)。
1.2 发展历程
G1的发展历程见证了Java垃圾回收技术的迭代:
- 2004年:G1回收器的核心论文首次发布,奠定了理论基础;
- 2009年:JDK 6u14版本中,G1作为实验性功能首次亮相;
- 2012年:JDK 7u4版本中,G1正式获得官方支持,可用于生产环境;
- 2017年:JDK 9版本中,G1成为默认垃圾回收器,同时官方废弃了CMS回收器,标志着G1时代的到来。
1.3 适用场景
G1的设计目标决定了它的适用范围:
- ✅ 同时追求吞吐量与低延迟:默认暂停目标为200ms,在保证吞吐量的同时,将STW(Stop-The-World)时间控制在可接受范围;
- ✅ 超大堆内存场景:将堆划分为多个Region,避免了传统回收器在大堆下回收时间不可控的问题;
- ✅ 关注内存碎片:整体采用标记-整理算法,Region间使用复制算法,从根本上避免了CMS标记-清除算法导致的内存碎片问题。
1.4 核心JVM参数
G1提供了丰富的JVM参数用于调优,核心参数如下:
| 参数 | 作用 |
|---|---|
-XX:+UseG1GC |
启用G1垃圾回收器(JDK8需手动开启,JDK9+默认开启) |
-XX:G1HeapRegionSize=size |
设置Region的大小,取值范围为1MB~32MB,且必须是2的幂次 |
-XX:MaxGCPauseMillis=time |
设置GC最大暂停时间目标(单位:毫秒),默认值为200ms |
-XX:InitiatingHeapOccupancyPercent=percent |
触发并发标记的堆内存占用阈值,默认值为45%(JDK9后可动态调整) |
二、G1垃圾回收核心阶段
G1的回收过程分为三大核心阶段,同时在极端情况下会退化至Full GC,各阶段逻辑如下:
2.1 Young Collection(新生代回收)
Young Collection是G1的基础回收阶段,会触发STW,核心逻辑是回收新生代Eden区和Survivor区的垃圾对象:
- Eden区回收 :将Eden区中存活的对象通过复制算法迁移至Survivor区;
- Survivor区回收 :遍历Survivor区存活对象,若对象年龄超过15次(可通过
-XX:MaxTenuringThreshold调整),则晋升至老年代Region;若年龄不足15次,则复制至另一个Survivor区。
2.2 Young Collection + Concurrent Mark(新生代回收 + 并发标记)
当老年代堆内存占用达到阈值(由-XX:InitiatingHeapOccupancyPercent指定,默认45%)时,G1会在Young Collection的基础上,开启并发标记阶段:
- 初始标记:在Young GC时同步完成GC Root的初始标记,此阶段会STW;
- 并发标记 :从GC Root开始,并发遍历老年代对象,标记存活对象,此阶段不会STW,与用户线程并行执行;
- 并发标记完成后,G1会进入Mixed Collection阶段。
2.3 Mixed Collection(混合回收)
Mixed Collection是G1的核心回收阶段,会对**新生代(E/S)和老年代(O)**进行全面回收,核心分为两个STW子阶段:
- 最终标记(Remark):暂停用户线程,完成并发标记阶段遗漏的存活对象标记,修正并发标记期间因用户线程操作导致的引用变化;
- 拷贝存活(Evacuation) :暂停用户线程,将存活对象复制到新的Region中,同时回收垃圾Region。此阶段并非回收所有老年代Region,而是优先回收垃圾占比最高、价值最大的Region ,以尽可能满足
-XX:MaxGCPauseMillis指定的暂停时间目标。
2.4 Full GC(退化场景)
基础概念先厘清
- Minor GC :特指新生代的垃圾回收,只回收Eden区和Survivor区,触发时STW时间通常很短。
- Full GC :对**整个堆(新生代+老年代+元空间)**进行全面回收,STW时间极长,是生产环境中要极力避免的情况。
不同回收器的Full GC触发逻辑
| 回收器 | Minor GC触发 | 老年代内存不足时的行为 | 是否会出现Full GC日志 |
|---|---|---|---|
| Serial/Parallel | 新生代不足 | 直接触发Full GC | 是,明确打印Full GC |
| CMS | 新生代不足 | 先尝试并发回收,若回收速度跟不上则退化Full GC | 正常并发回收时无,退化时才打印Full GC |
| G1 | 新生代不足 | 先尝试混合回收(Mixed GC),若回收速度跟不上则退化Full GC | 正常混合回收时无,退化时才执行Full GC |
Serial/Parallel GC :
• Minor GC:新生代(Eden区)内存不足时触发,只回收新生代。
• Full GC:老年代内存不足时触发,对整个堆进行整理式回收,后台日志会明确打印 Full GC 字样。
CMS GC :
• Minor GC:新生代内存不足时触发,回收新生代。
• 老年代回收:当老年代占比达到阈值时,启动并发回收流程 (初始标记→并发标记→重新标记→并发清除),大部分阶段与用户线程并行,STW极短;只要回收速度≥垃圾产生速度,后台不会出现Full GC字样 ;只有当并发回收速度跟不上、老年代被填满时,才会退化至Serial Old,执行真正的Full GC。
G1 GC :
• Minor GC:新生代(Eden区)内存不足时触发,只回收新生代Region。
• 混合回收(Mixed GC):老年代占比达到阈值后,先并发标记,再执行混合回收 ,选择性回收部分高价值老年代Region+新生代Region;正常混合回收时日志仅显示Mixed GC,不会打印Full GC ;
• 退化至Full GC:只有当混合回收速度完全跟不上分配、堆被占满时,G1才会触发Full GC;注意:JDK9及以上版本中,G1的Full GC已改为多线程执行(基于Parallel Old算法),而非早期JDK8的单线程Serial Old,但即便如此,Full GC的STW时间依然远长于混合回收,仍是极端性能灾难,必须通过调优避免。
三、G1核心底层机制解析
G1的高效回收依赖于多个底层机制的支撑,这些机制是理解G1性能优势的关键:
3.1 跨代引用与Remembered Set
在新生代回收时,若遍历整个老年代对象来查找跨代引用(老年代引用新生代),效率极低。G1通过Card Table 和Remembered Set解决此问题:
- Card Table :将老年代每个Region划分为512KB大小的Card(卡),若某Card中的对象引用了新生代对象,则将该Card标记为脏卡(Dirty Card);
- Remembered Set:每个Region维护一个Remembered Set,记录引用当前Region对象的其他Region Card;
- 异步更新机制 :
- 通过
post-write barrier(写后屏障)捕获对象引用的变更事件,触发后不会立刻完成脏卡的更新 ,而是先将脏卡信息写入dirty card queue(脏卡队列); - JVM后台运行的并发refinement线程 会异步消费
dirty card queue中的脏卡信息,将其更新至对应Region的Remembered Set中; - 这种异步设计的核心目的是避免写屏障直接操作Remembered Set带来的性能阻塞,保证用户线程的执行效率;
- 通过
- 新生代回收时,仅需扫描脏卡对应的Remembered Set,大幅提升扫描效率。
3.2 Remark重标记机制(SATB + 写前屏障)
并发标记阶段,标记线程与用户线程并行执行,会出现引用变更遗漏 的核心问题(比如标记线程标记C对象为垃圾后,用户线程又重新引用了C对象)。G1通过SATB(Snapshot At The Beginning,初始快照标记)+ pre-write barrier(写前屏障)+ satb_mark_queue 三位一体的机制解决该问题,以下是完整逻辑:
1. SATB核心思想
SATB的核心是:以"并发标记开始时的对象引用图"为快照基准,只要对象在快照中是可达的,就标记为存活;即使并发标记过程中对象引用被删除/新增,也通过写屏障+队列记录,在Remark阶段修正,避免误回收。
2. 核心问题场景(以C对象为例)
// 并发标记过程中发生的引用变更
A.obj → C(并发标记开始时,A引用C,标记线程开始遍历A)
↓ 标记线程遍历到A时,发现A.obj=null(用户线程刚修改),标记C为"垃圾"
↓ 标记线程还未完成整个并发标记,用户线程又执行:B.obj = C(重新引用C)
// 若不处理,C会被误标记为垃圾,最终被回收,导致空指针异常
3. pre-write barrier(写前屏障)的具体作用
写前屏障是嵌入在对象引用赋值指令前的一段代码 ,只要用户线程执行"对象引用变更"操作(如A.obj = null、B.obj = C),就会触发该屏障。伪代码如下:
// 写前屏障伪代码(JVM底层C++实现)
void pre_write_barrier(Object* src, Object* old_ref) {
// 1. 若旧引用(old_ref)非空,且未被标记为"存活"
if (old_ref != nullptr && !is_marked(old_ref)) {
// 2. 将旧引用对象(如上述场景的C)放入satb_mark_queue
satb_mark_queue.push(old_ref);
// 3. 强制标记该对象为"存活",避免被误回收
mark_object(old_ref);
}
}
- 触发时机:所有对象引用赋值操作前 (如
obj.field = newObj、obj.field = null); - 核心动作:捕获"即将被覆盖/删除的旧引用对象",将其入队并临时标记为存活,确保并发标记阶段不会误判为垃圾。
4. satb_mark_queue(SATB标记队列)的作用
satb_mark_queue是线程本地队列(TLQ),每个用户线程都有独立队列,避免锁竞争;- 写前屏障将需要修正的对象(如上述C对象)存入队列,不阻塞用户线程,异步等待处理;
- 队列仅存储"并发标记期间引用变更的对象",数量远小于全堆对象,保证后续处理效率;
- 关键补充 :队列存储的是整个并发标记阶段内所有发生引用变更的对象 ,无论该对象是否被垃圾回收线程处理过 ------ 只要用户线程在并发标记期间修改了对象引用,就会触发写前屏障将相关对象入队,和GC线程是否遍历/处理过该对象没有关系。
两种典型入队场景(结合C对象)
- 场景1:GC线程还没处理C对象,用户线程先改了引用
// 并发标记开始 → 初始快照:A.obj → C
↓ GC线程正在遍历其他对象(还没轮到A和C)
↓ 用户线程执行:A.obj = null(删除对C的引用)
→ 触发写前屏障:C是"即将被删除的旧引用对象",直接入队satb_mark_queue
→ 后续GC线程遍历到A时,即使发现A.obj=null,也会因为C已入队,在Remark阶段重新检查 - 场景2:GC线程已处理C对象,用户线程再改引用
// 并发标记开始 → 初始快照:A.obj → C
↓ GC线程遍历到A,发现A.obj=null(用户线程刚改),标记C为"垃圾"
↓ GC线程继续遍历其他对象(还没结束并发标记)
↓ 用户线程执行:B.obj = C(重新引用C)
→ 触发写前屏障:C是"新增引用的对象",依然入队satb_mark_queue
→ Remark阶段处理队列时,会发现C被B引用,修正标记为"存活"
为什么要"不管GC是否处理过都入队"?
G1的SATB核心是"以并发标记开始时的引用图为基准,兜底所有中途的引用变更" ------ 如果只记录"GC处理过的对象",会漏掉两种致命情况:
- 漏判存活:GC还没遍历到的对象,引用被删了 → 若不入队,GC后续遍历到会直接标记为垃圾,即使之后又被重新引用;
- 误判垃圾:GC刚标记为垃圾的对象,又被重新引用 → 若不入队,会被当成垃圾回收。
只有"全覆盖"并发标记阶段的所有引用变更,才能在Remark阶段通过队列统一修正,保证标记结果100%准确。
5. Remark阶段(STW)的修正逻辑
- 暂停所有用户线程,保证引用不再变更;
- 消费satb_mark_queue队列:遍历队列中的所有对象(如上述C对象),重新追溯其可达性;
- 修正标记结果:若对象仍可达(如C被B引用),则确认标记为"存活";若确实不可达,仍标记为"垃圾";
- 补充扫描:扫描GC Root(如线程栈、常量池),确保最新的可达对象都被标记。
6. 最终效果
通过写前屏障+SATB队列+Remark修正,上述场景中的C对象会被重新标记为"存活",避免了误回收;同时写前屏障的异步入队设计,仅带来极小的性能开销,保证了并发标记阶段的吞吐量。
3.3 JDK8u20 字符串去重(String Deduplication)
JDK8u20引入了字符串去重功能,用于优化字符串内存占用:
- 核心逻辑 :将新分配的字符串放入队列,新生代回收时G1并发检查队列中字符串的
char[],若值相同则让多个字符串对象共享同一个char[]; - 优缺点 :
✅ 优点:大幅节省内存,尤其适用于大量重复字符串的场景(如日志、配置);
❌ 缺点:略微占用CPU时间,新生代回收时间略有增加; - 配置参数 :
-XX:+UseStringDeduplication(默认开启); - 与
String.intern()的区别 :前者共享char[],后者复用字符串对象本身。
3.4 JDK8u40 并发标记类卸载
JDK8u40支持在并发标记阶段完成类卸载:
- 并发标记完成后,JVM可识别不再被使用的类加载器;
- 当一个类加载器的所有类都无存活对象引用时,卸载该类加载器加载的所有类;
- 配置参数 :
-XX:ClassUnloadingWithConcurrentMark(默认启用)。
3.5 JDK8u60 巨型对象(Humongous Object)回收优化
JDK8u60对巨型对象的回收进行了优化:
- 巨型对象定义:大小超过Region一半的对象;
- 回收特性:G1不会对巨型对象进行拷贝,避免拷贝开销;回收时优先考虑巨型对象;
- 回收优化:G1会跟踪老年代中所有指向巨型对象的incoming引用,若巨型对象的incoming引用为0,可在新生代回收阶段直接处理,无需等待混合回收。
四、JDK版本演进:G1的持续优化
G1在不同JDK版本中持续迭代,核心优化如下:
4.1 JDK9:并发标记起始时间动态调整
JDK9之前,并发标记的触发阈值由-XX:InitiatingHeapOccupancyPercent固定指定(默认45%),若阈值设置不合理,易导致并发标记未完成即堆满,退化至Full GC。JDK9对此进行了优化:
- 并发标记必须在堆占满前完成,否则退化至Full GC;
- JDK9支持动态调整并发标记起始阈值:通过采样堆内存使用情况,自动调整触发时机,同时保留安全空档空间,尽可能避免退化至Full GC;
-XX:InitiatingHeapOccupancyPercent仅用于设置初始阈值,后续由JVM动态调整。
4.2 JDK9+:更高效的回收实现
JDK9及后续版本对G1进行了大量性能增强与Bug修复:
- 250+项性能增强,涵盖回收算法、内存管理、并发调度等方面;
- 180+个Bug修复,提升了G1的稳定性与可靠性;
- 官方文档:Java 12 G1 Tuning Guide,G1已成为成熟、稳定的企业级垃圾回收器。
五、总结与实战建议
5.1 核心总结
G1垃圾回收器是Java生态中面向大堆、低延迟场景的最优解,核心优势包括:
- Region化堆管理:打破物理分代,灵活回收高价值区域;
- 标记-整理+复制算法:从根本上避免内存碎片;
- 可预测的暂停时间:通过优先回收高价值Region,将STW时间控制在目标范围内;
- 持续的版本优化:从JDK8到JDK9+,G1在并发标记、内存占用、回收效率等方面持续迭代。
5.2 实战调优建议
- 合理设置Region大小 :根据堆内存大小调整
-XX:G1HeapRegionSize,保证Region数量在2048个以内; - 设定合理的暂停目标 :
-XX:MaxGCPauseMillis不宜过小(如<100ms),否则会导致频繁回收、吞吐量下降; - 避免Full GC :通过调优
-XX:InitiatingHeapOccupancyPercent(JDK9前)或依赖JDK9动态调整,保证并发标记及时完成; - 场景化使用字符串去重 :若应用存在大量重复字符串,可保留
-XX:+UseStringDeduplication,否则可关闭以减少新生代回收时间; - 监控与分析 :通过
jstat、jmap、VisualVM等工具监控G1回收情况,重点关注暂停时间、回收频率、Full GC触发次数。
总结
- G1的跨代引用处理采用异步设计:写后屏障将脏卡入队,由refinement线程异步更新Remembered Set,避免阻塞用户线程;
- 并发标记的引用变更遗漏问题通过SATB+写前屏障解决:写前屏障捕获旧引用对象入队并临时标记存活(无论GC是否处理过该对象),Remark阶段(STW)消费队列修正标记结果,避免对象误回收;
- G1退化至Full GC的特性随版本优化:JDK9及以上版本的Full GC为多线程执行(Parallel Old),但仍远慢于混合回收,需通过调优避免触发。
结语
G1垃圾回收器是Java虚拟机发展的里程碑,它完美平衡了大堆内存、低延迟与内存碎片问题,成为现代Java应用的首选回收器。深入理解G1的核心原理与版本演进,不仅能帮助开发者应对面试中的高频考点,更能在生产环境中实现高效的内存调优,提升系统稳定性与性能。
参考资料 :
• Oracle官方文档:Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide
• 《深入理解Java虚拟机:JVM高级特性与最佳实践》(周志明)