JVM 中一次完整的 GC 流程是怎样的?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 内存模型掌握度:面试官不只是想知道 "GC 是垃圾回收",而是想看你能不能把堆内存的分代结构、各区域存什么、什么时候触发哪种 GC 串成一条线讲清楚。

  2. GC 机制理解深度:能否说清楚 Minor GC、Major GC、Full GC 的触发条件、回收区域、停顿影响,以及它们之间的关联。

  3. 对象流转认知:是否理解对象从 Eden → Survivor → Old 的晋升过程,以及 GC 如何决定哪些对象该回收、哪些该保留。

核心答案

一次完整的 GC 流程,本质上是 对象在不同分代之间流转 + 垃圾回收器在不同区域执行回收 的过程。核心主线就一句话:

新对象在 Eden 区出生 → Minor GC 回收年轻代 → 存活对象晋升老年代 → 老年代满了触发 Full GC(或 Major GC)回收整个堆。

下面这张表先帮你建立全局认知:

GC 类型 回收区域 触发条件 停顿影响
Minor GC 年轻代(Eden + Survivor) Eden 区空间不足 较小,通常几毫秒~几十毫秒
Major GC 老年代 老年代空间不足(具体看收集器) 较大
Full GC 整个堆 + 方法区(元空间) 多种条件触发 最大,应尽量避免

深度解析

一、堆内存分代结构

先搞清楚 "战场" 长什么样。JVM 堆内存采用 分代收集 策略,分为年轻代和老年代:

上图展示了 JVM 堆内存的分代布局,几个关键比例需要记住:

  • 年轻代 : 老年代 = 1 : 2(默认比例,可通过 -XX:NewRatio 调整)
  • Eden : S0 : S1 = 8 : 1 : 1(默认比例,可通过 -XX:SurvivorRatio 调整)
  • 年轻代又细分为 1 个 Eden 区和 2 个 Survivor 区(S0 和 S1,也叫 From 和 To)

这个 8:1:1 的比例设计是有讲究的——IBM 研究表明,98% 的对象都是朝生夕死的,所以 Eden 区分配最大空间,而两块 Survivor 轮换使用,保证始终有一块是空的。

二、对象分配与 Minor GC 流程

这是 GC 流程中最频繁发生的一段。我把它拆成几个步骤来讲:

第 1 步:新对象分配到 Eden 区

// 每次 new 一个对象,JVM 优先尝试在 Eden 区分配
User user = new User("小明");

正常情况下,新对象直接分配在 Eden 区。但如果遇到 大对象(比如很长的数组或大字符串),JVM 会直接将其分配到老年代,避免在 Eden 和 Survivor 之间来回复制。阈值由 -XX:PretenureSizeThreshold 控制。

第 2 步:Eden 区空间不足,触发 Minor GC

当 Eden 区放不下新对象时,触发 Minor GC(也叫 Young GC)。

第 3 步:GC Root 可达性分析,标记存活对象

JVM 从 GC Roots 出发,通过引用链判断哪些对象是存活的,哪些是垃圾。GC Roots 包括:

  • 虚拟机栈中的局部变量引用
  • 方法区中类的静态变量引用
  • 方法区中常量引用
  • 本地方法栈中 JNI 引用
  • JVM 内部引用(基本类型对应的 Class 对象、常驻异常对象、类加载器等)

第 4 步:复制算法,存活对象移到 Survivor 区

上图展示了 Minor GC 的核心过程:

  • JVM 使用 复制算法,将 Eden 区和当前使用的 Survivor 区(比如 S0)中存活的对象,复制到另一个空闲的 Survivor 区(S1)
  • 然后清空 Eden 和 S0,S1 成为新的 "From Survivor",S0 变成 "To Survivor"
  • 两块 Survivor 角色互换,保证始终有一块是空的

这个过程很快,因为年轻代本身就不大,而且大部分对象都是垃圾,需要复制的存活对象很少。

第 5 步:对象年龄判断,决定是否晋升老年代

每经历一次 Minor GC 仍然存活的对象,年龄(age)加 1。当年龄达到阈值(默认 15,由 -XX:MaxTenuringThreshold 控制),就晋升到老年代。

不过这里有个细节,很多人不知道:JVM 还有一个动态年龄判断机制。如果 Survivor 区中相同年龄的所有对象大小总和超过 Survivor 空间的一半,大于等于该年龄的对象也会直接晋升老年代,不用等到 MaxTenuringThreshold

三、Full GC 触发条件

Full GC 是最 "重" 的 GC,会回收整个堆和方法区,停顿时间也最长。生产上应该尽量减少 Full GC 的频率。触发条件主要有这几个:

  1. 老年代空间不足:老年代对象越来越多,放不下了。常见原因是大对象直接进老年代、长期存活的对象堆积、内存泄漏等。

  2. System.gc() 被调用:虽然只是 "建议" JVM 进行 GC,但很多场景下 JVM 会响应。生产环境建议用 -XX:+DisableExplicitGC 禁掉。

  3. 元空间(Metaspace)不足:JDK 8 之后方法区用 Metaspace 实现,当 Metaspace 达到 -XX:MaxMetaspaceSize 阈值时,也会触发 Full GC。

  4. Minor GC 后存活对象大于老年代剩余空间:Minor GC 结束后,要晋升到老年代的对象大小超过了老年代当前可用空间,这时候会先触发一次 Full GC。

  5. 空间分配担保失败:JDK 6 Update 24 之前,Minor GC 前会检查老年代最大可用连续空间是否大于年轻代所有对象总空间(或历次晋升对象的平均大小),如果不满足且不允许担保失败,就会先触发 Full GC。

四、完整 GC 流程总览

把上面所有环节串起来,一次完整的 GC 流程如下:

上图把整个 GC 流程串起来了,从对象分配到最终的 OOM,核心路径非常清晰。几个关键节点再强调一下:

  • Minor GC 很频繁,通常几毫秒就搞定,对应用影响小
  • Full GC 是大杀器,STW(Stop The World)时间长,可能导致服务超时
  • 如果 Full GC 之后空间还是不够,那就抛出 OutOfMemoryError,GG

五、常用 GC 日志参数

实际开发中,排查 GC 问题离不开日志。这几个参数建议加上:

# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log

# JDK 11+(日志参数变了)
-Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags

面试高频追问

  1. 追问:CMS 和 G1 的 Full GC 有什么区别?

    CMS 的 Full GC 会退化成 Serial Old 收集器,单线程回收整个堆,非常慢。G1 理论上不需要 Full GC,它通过逐步回收 Region 来避免;但如果 Mixed GC 跟不上垃圾产生速度,退化为 Serial Old 做 Full GC 也很惨。

  2. 追问:如何减少 Full GC 的频率?

    几个实操方向:增大堆内存或调整年轻代比例让 Minor GC 更 "够用"、排查内存泄漏、避免大对象频繁分配、选择合适的垃圾收集器(G1/ZGC)、确保 Survivor 区别太小导致对象过早晋升。

  3. 追问:什么情况下对象会直接进入老年代?

    三个场景:大对象超过 PretenureSizeThreshold 直接分配到老年代;长期存活对象年龄达到阈值晋升;Survivor 区中同年龄对象总大小超过 Survivor 空间一半时触发动态年龄判断晋升。

常见面试变体

  • "说一下 JVM 的垃圾回收机制"
  • "Minor GC 和 Full GC 的区别是什么?"
  • "对象从年轻代晋升到老年代的条件有哪些?"
  • "什么情况下会触发 Full GC?如何避免?"

记忆口诀

分代收集三步走:Eden 出生 → Minor GC 存活者搬家(Survivor)→ 老了进 Old。触发链:Eden 满 → Minor GC → Old 满 → Full GC → 还满 → OOM

总结

JVM 的 GC 流程本质就是分代收集思想的具体落地:新对象在 Eden 区分配,Minor GC 用复制算法高效回收年轻代,存活对象逐步晋升老年代,老年代满了再触发 Full GC。面试中把这条线讲清楚,再带上 Full GC 触发条件和对象晋升细节,基本就能拿到高分。