JVM 中一次完整的 GC 流程是怎样的?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
这个问题是 JVM 内存管理的高频考题,面试官主要想考察你对以下内容的掌握程度:
- 是否理解分代收集理论:知道为什么堆要分年轻代和老年代,以及不同代的特点。
- GC 触发条件与流程:清楚 Minor GC、Major GC、Full GC 分别在什么情况下触发,执行过程是怎样的。
- 对象分配与晋升机制:对象是如何从 Eden 区分配到老年代的,年龄阈值、动态年龄判断、空间分配担保等细节。
- 不同垃圾收集器的差异:了解至少一种主流收集器(如 G1、CMS、Parallel)的 GC 流程,并能对比说明。
- GC 日志与调优基础:通过 GC 流程的描述,间接反映你是否能读懂 GC 日志,并具备初步调优思路。
核心答案
在 HotSpot 虚拟机中,一次完整的 GC 流程并不是单一的步骤,而是根据对象的生命周期分阶段进行。简单来说:
- 对象优先在 Eden 分配:新创建的对象首先放在年轻代的 Eden 区。
- Eden 区满触发 Minor GC:当 Eden 区没有足够空间分配新对象时,JVM 会执行一次 Minor GC(年轻代垃圾回收)。此时,存活的对象会被复制到 Survivor 区(S0 或 S1),同时对象年龄加 1。
- 对象晋升:经过多次 Minor GC 后,年龄达到阈值(默认 15)的对象,或者动态年龄判断符合条件的对象,会被晋升到老年代。
- 老年代满触发 Major GC / Full GC:当老年代空间不足(例如晋升对象放不下,或大对象直接进入老年代导致空间不足),就会触发 Major GC 或 Full GC。Major GC 通常只清理老年代,但很多情况下会导致 Full GC(清理整个堆,包括年轻代、老年代和方法区)。
- 永久代/元空间满也会触发 Full GC:在 JDK 8 之前,永久代满会触发 Full GC;JDK 8+ 使用本地内存的元空间,通常不会触发 GC,但如果元空间设置了上限且达到上限,也会触发 Full GC。
需要注意的是,不同垃圾收集器的具体流程有差异。比如 G1 收集器打破了物理上的分代,将堆划分为多个 Region,通过混合 GC(Mixed GC)同时回收年轻代和老年代的部分 Region。
深度解析
1. 年轻代 GC(Minor GC)详细流程
我们以最经典的 Parallel Scavenge(JDK 8 默认)或 ParNew 配合 CMS 为例,看一次 Minor GC 的完整过程:
- 对象分配:新对象通常分配在 Eden 区。如果 Eden 区空间不足,JVM 会尝试分配担保,看老年代是否有足够空间容纳 Minor GC 后可能晋升的对象。
- 触发 Minor GC:当 Eden 区无法分配新对象时,JVM 停止应用程序线程(Stop-The-World,STW),开始 Minor GC。
- 标记存活对象:从 GC Roots 出发,标记所有在年轻代中仍然被引用的对象。
- 复制存活对象:将 Eden 区和当前使用中的 Survivor 区(比如 From 区)中的所有存活对象,复制到另一个空闲的 Survivor 区(To 区)。复制过程中,对象的年龄会加 1。
- 清理 Eden 和 From 区:清空 Eden 和 From 区,这些区域现在可以用于分配新对象。
- 对象晋升:如果 Survivor 区无法容纳某些存活对象,或者对象年龄达到阈值(-XX:+MaxTenuringThreshold 设置),这些对象会被直接复制到老年代。
- 恢复应用线程:Minor GC 结束,应用程序继续运行。
注意:Minor GC 会 STW,但通常时间很短。Parallel 收集器可以通过多线程并行执行来减少 STW 时间。
2. 老年代 GC(Major GC / Full GC)流程对比
老年代 GC 通常更耗时,且往往伴随着 STW 时间更长。不同收集器的 Full GC 流程差异很大:
- Serial Old / Parallel Old(标记-压缩算法):
- 同样 STW,对老年代进行标记、压缩整理,消除内存碎片。压缩阶段需要移动对象,所以停顿时间与老年代存活对象数量成正比。
- CMS(并发标记-清除):
- CMS 的目标是减少 STW 时间,它的 Full GC 其实很少发生,但一旦发生(比如并发模式失败),会退化为 Serial Old 单线程收集,停顿时间极长。
- 正常 CMS 流程分 4 步:初始标记(STW)、并发标记、重新标记(STW)、并发清除。但并发清除阶段不会整理内存,所以会产生碎片。
- G1(混合 GC):
- G1 没有传统意义上的 Full GC,而是通过混合 GC 来同时回收年轻代和老年代的 Region。
- 当老年代占用率达到阈值(
-XX:InitiatingHeapOccupancyPercent)时,G1 会启动并发标记周期。之后会进行混合 GC,选择部分存活对象少的 Region 优先回收,避免一次性全堆回收。 - 如果并发标记过程中老年代被填满,或者分配巨型对象失败,G1 会退化为 Full GC,使用 Serial Old 单线程进行全堆整理,这是需要极力避免的情况。
- ZGC(JDK 11+ 实验性,JDK 15+ 正式):
- ZGC 的 GC 流程几乎全是并发的,停顿时间极短(<10ms),但原理更为复杂,涉及染色指针、读屏障等技术。
3. 对象晋升的细节
面试中常问的 “对象何时进入老年代” 其实包含多个规则:
- 年龄阈值:对象每熬过一次 Minor GC,年龄就加 1,当年龄超过
MaxTenuringThreshold(默认 15)时,晋升到老年代。 - 动态年龄判断:HotSpot 并不是等到年龄达到阈值才晋升,而是会统计 Survivor 中同年龄对象的大小总和,如果超过 Survivor 空间的一半,那么年龄大于等于该值的对象就会直接晋升(即使没到阈值)。
- 空间分配担保:在 Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于年轻代所有对象总大小。如果大于,则 Minor GC 是安全的;如果小于,会检查
HandlePromotionFailure参数是否允许担保失败。如果允许,再检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试 Minor GC,否则改为 Full GC。- 在 JDK 6 Update 24 之后,
HandlePromotionFailure参数已失效,规则简化:只要老年代的连续空间大于新生代对象总大小或者历次晋升平均大小,就会进行 Minor GC,否则 Full GC。
- 在 JDK 6 Update 24 之后,
4. 代码示例与 GC 日志观察
你可以通过一个小程序,配合 JVM 参数打印 GC 日志,观察 GC 流程:
// 设置 JVM 参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
public class GCTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 这里可能触发一次 Minor GC
}
}
日志输出会显示 Eden 区的分配情况,以及 Minor GC 前后堆内存的变化,从中可以看出对象是否晋升到老年代。
5. 常见误区
- 误区一:认为 Full GC 一定会回收整个堆。其实某些收集器(如 CMS)的并发失败导致的 Full GC 只回收老年代,但停顿时间极长。
- 误区二:混淆 Minor GC 和 Full GC 的触发条件。比如调用
System.gc()只是建议 JVM 进行 Full GC,但不一定立即执行,取决于DisableExplicitGC参数。 - 误区三:忽略动态年龄判断。只记得年龄阈值 15,不知道 Survivor 空间一半规则也会导致提前晋升。
- 误区四:认为 G1 没有 Full GC。G1 的 Full GC 虽然少见,但一旦发生(比如并发标记失败、巨型对象分配失败),会退化为串行收集,性能极差,需通过调优避免。
总结
一次完整的 GC 流程本质上是 JVM 对堆内存中对象的 “分代清理”,核心是 Minor GC(年轻代快速回收) 配合 Major/Full GC(老年代整理)。了解 GC 流程,不仅能帮你回答面试题,更是定位线上内存问题(如频繁 Full GC、GC 时间过长)的基础。实际调优时,需结合收集器特点,调整分代大小、晋升阈值等参数,让 GC 频率和停顿时间达到平衡。