前言
JVM 的垃圾回收是 Java 面试里的高频重点,也是线上问题排查时绕不开的一块。
很多人学 GC 的时候容易陷入两个误区:
-
只背概念,不知道这些机制为什么存在
-
只记回收器名字,不知道它们到底解决了什么问题
这篇笔记的目标不是把 GC 讲得特别"学术",而是帮你建立一套完整的理解框架:
先知道 JVM 为什么需要 GC,再理解对象怎么判断存活,然后把回收算法、分代思想、回收器和 GC 类型串起来。
一、为什么 JVM 需要垃圾回收
Java 和 C、C++ 最大的一个区别,就是 Java 不需要程序员手动释放内存。
对象创建后,JVM 会负责管理它的生命周期。当一个对象已经不再被程序使用时,就应该把它占用的内存回收掉,否则对象越积越多,最终就会导致内存溢出。
所以 GC 的本质就是一句话:
自动找出"已经没用的对象",并把它们占用的内存清理掉。
这样做的好处是:
-
降低手动管理内存的复杂度
-
减少野指针、重复释放等问题
-
让开发者更专注于业务逻辑
但代价也很明显:
-
GC 会占用 CPU 资源
-
GC 期间可能发生停顿
-
如果内存分配和回收策略不合理,性能会明显抖动
二、先补一个背景:JVM 运行时内存区域
在讲 GC 前,先明确一件事:
不是 JVM 所有内存区域都会发生垃圾回收,GC 重点关注的是堆和方法区。
JVM 运行时内存大致可以分成下面几块:
1. 程序计数器
线程私有,记录当前线程执行到哪条字节码指令。这个区域很小,也基本不涉及 GC。
2. 虚拟机栈
线程私有,存放方法调用过程中的栈帧、局部变量表等。方法执行结束,对应栈帧出栈,内存自然释放,也不属于典型 GC 处理范围。
3. 本地方法栈
和虚拟机栈类似,只不过服务的是 Native 方法。
4. 堆
堆是 Java 对象实例最主要的存储区域,也是垃圾回收最核心的区域。
5. 方法区
方法区主要存储类元信息、常量、静态变量、JIT 编译后的代码缓存等。HotSpot 里早期常提永久代,后来更多提元空间。
一句话总结:
GC 主要回收的是堆中的对象,以及方法区中不再使用的类和常量。
三、如何判断对象已经"死了"
垃圾回收的第一步,不是回收,而是判断哪些对象还能用,哪些对象已经没用了。
1. 引用计数法
引用计数法的思路很直观:
-
给对象加一个引用计数器
-
每被一个地方引用,计数器加一
-
每失去一个引用,计数器减一
-
当计数器变成 0,就说明对象可以回收
这个方法看起来很简单,但有一个致命问题:
它无法很好地解决对象循环引用的问题。
比如 A 引用 B,B 也引用 A,虽然这两个对象其实都已经不再被外部使用,但它们的计数器都不为 0,就无法回收。
2. 可达性分析
JVM 主流实现采用的是可达性分析算法。
它的核心思想是:
从一组叫作 GC Roots 的对象作为起点往下搜索,能被这些起点直接或间接关联到的对象就是"存活对象",反之就是可回收对象。
常见的 GC Roots 包括:
-
虚拟机栈中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈 JNI 引用的对象
-
被同步锁持有的对象
所以判断对象是否存活,不是看"有没有引用",而是看:
它是否还能从 GC Roots 追踪到。
四、Java 中几种常见引用类型
Java 里不只有"有引用"和"没引用"两种状态,还进一步细分了引用强度。
1. 强引用
最普通的引用就是强引用,比如:
Object obj = new Object();
只要强引用还在,GC 一般就不会回收这个对象。
2. 软引用
软引用用于描述那些"有用但不是必须"的对象。内存不足时,JVM 会优先回收软引用对象。
典型场景是缓存。
3. 弱引用
弱引用比软引用更弱。只要发生 GC,不管当前内存紧不紧张,弱引用关联的对象都可能被回收。
典型场景是 ThreadLocalMap 中的 Key。
4. 虚引用
虚引用几乎不影响对象生命周期,主要用来在对象被回收时收到通知,常用于更底层的资源管理。
一句话理解:
强引用最稳,软引用适合缓存,弱引用更容易被回收,虚引用主要是配合回收通知。
五、垃圾回收的基础算法有哪些
这部分是 GC 的底层核心。后面的分代回收器,本质上都是在这些基础算法上做组合和优化。
1. 标记-清除算法
流程分两步:
-
先标记出所有需要回收的对象
-
再统一清除这些对象
优点是实现简单。
缺点主要有两个:
-
效率一般
-
会产生内存碎片
内存碎片的意思是:虽然总空闲内存够,但它们不连续,大对象可能没法直接分配。
2. 复制算法
复制算法的思路是:
-
把内存分成两块
-
每次只使用其中一块
-
发生 GC 时,把存活对象复制到另一块
-
然后一次性清空原来的那一整块
优点:
-
实现简单
-
没有内存碎片
-
适合"存活对象少、死亡对象多"的场景
缺点:
-
需要额外预留一部分空间
-
如果存活对象很多,复制成本高
3. 标记-整理算法
标记-整理可以看成标记-清除的改良版。
流程是:
-
标记存活对象
-
让所有存活对象向一端移动
-
清理边界之外的内存
优点是:
-
没有内存碎片
-
适合老年代这种对象存活率较高的场景
缺点是:
- 整理过程有移动成本
4. 分代收集算法
严格来说,分代收集不是一个基础算法,而是一种设计思想。
它的核心判断是:
不同生命周期的对象,适合不同的回收策略。
因为大多数 Java 对象都"朝生夕死",少数对象会长期存活,所以 JVM 把堆划分为不同区域,针对不同区域采用不同算法,这就是分代回收思想。
六、为什么要分代回收
分代回收的本质依据是对象生命周期特征不同。
大多数对象都有两个典型特点:
-
刚创建出来很快就没用了
-
少量对象会一直活很久
所以 JVM 通常会把堆分成:
-
新生代
-
老年代
有些资料还会额外提永久代或元空间,但那更多属于方法区的实现,不是传统意义上的"堆分代"。
1. 新生代
新生代主要放新创建的对象。这里对象数量多,但大部分死得快,所以适合使用复制算法。
2. 老年代
老年代主要放存活时间较长的对象。这里对象存活率高,不适合简单复制,所以更常采用标记-清除 或标记-整理思想。
一句话总结:
新生代追求"快速回收大量短命对象",老年代关注"稳定回收长期存活对象"。
七、新生代为什么分 Eden、Survivor
新生代一般不是一整块连续区域,而是进一步分成:
-
Eden 区
-
Survivor From 区
-
Survivor To 区
默认情况下,大部分对象会先在 Eden 里分配。
当 Eden 空间不够时,会触发一次 Minor GC。GC 后还活着的对象会被复制到 Survivor 区。之后每经历一次回收还活着,对象年龄就会加一。
当对象年龄达到某个阈值后,就会晋升到老年代。
这里常说的新生代比例,一般是:
Eden : Survivor From : Survivor To = 8 : 1 : 1
这样设计的原因是:
-
大多数对象都死得很快,不需要给 Survivor 留太大空间
-
保留两个 Survivor 区,方便复制和年龄统计
八、对象什么时候进入老年代
对象进入老年代,常见有几种情况:
1. 年龄达到阈值
对象每在 Survivor 区熬过一次 GC,年龄就会加一。达到晋升阈值后进入老年代。
2. 大对象直接进入老年代
如果对象特别大,比如超大的数组,为了避免在 Eden 和 Survivor 之间来回复制,JVM 可能会让它直接进入老年代。
3. 动态年龄判断
如果 Survivor 中相同年龄对象的总大小超过了某个比例,不一定非得等到设定阈值,也可能提前晋升老年代。
九、GC 的几种常见类型
这部分面试里很高频,最好能准确区分。
1. Minor GC
也叫 Young GC,发生在新生代。
特点:
-
频率高
-
回收速度快
-
停顿时间通常较短
因为新生代大部分对象都会在一次次 Minor GC 中被直接清掉。
2. Major GC
Major GC 一般指针对老年代的 GC。
但要注意:
在不同资料和不同实现里,Major GC 的叫法并不总是完全统一。
所以面试里更稳妥的说法是:
-
Minor GC:新生代回收
-
Full GC:整个堆甚至连方法区一起回收
3. Full GC
Full GC 往往会回收:
-
新生代
-
老年代
-
方法区
Full GC 的特点通常是:
-
停顿时间长
-
对系统影响大
-
应尽量避免频繁发生
4. Mixed GC
Mixed GC 是 G1 里比较典型的概念。
它不是只回收新生代,也不是整个堆全回收,而是:
在回收新生代的同时,选择一部分收益高的老年代 Region 一起回收。
这样既能控制停顿,又能逐步回收老年代。
十、经典垃圾回收器怎么理解
回收算法解决的是"怎么回收",回收器解决的是"谁来执行这些算法,以及怎么权衡吞吐量、停顿时间和资源消耗"。
十一、Serial 回收器
Serial 是最基础、最简单的收集器。
特点:
-
单线程回收
-
发生 GC 时必须 Stop-The-World
-
实现简单,额外开销小
它适合:
-
单核环境
-
Client 模式
-
内存不大、对停顿不敏感的场景
它的缺点也很明显:
- 一旦堆大、并发高,停顿会比较明显
十二、ParNew 回收器
ParNew 可以理解成 Serial 的多线程版本,主要工作在新生代。
特点:
-
多线程并行回收
-
仍然会 STW
-
在多核机器上比 Serial 更高效
它过去经常和 CMS 搭配使用。
十三、Parallel Scavenge 回收器
Parallel Scavenge 也是新生代回收器,重点不是尽量缩短单次停顿,而是追求高吞吐量。
这里的吞吐量可以简单理解为:
用户代码执行时间 /(用户代码执行时间 + 垃圾回收时间)
所以它适合:
-
后台计算
-
批处理任务
-
更看重整体吞吐而不是单次响应时间的场景
它常和 Parallel Old 搭配。
十四、Serial Old 和 Parallel Old
这两个都是老年代回收器。
1. Serial Old
Serial Old 是 Serial 的老年代版本,单线程,通常采用标记-整理算法。
2. Parallel Old
Parallel Old 是 Parallel Scavenge 的老年代搭档,多线程,关注吞吐量。
如果应用更关注整体处理能力而不是响应延迟,这套组合会比较常见。
十五、CMS 回收器
CMS,全称 Concurrent Mark Sweep,是一款以低停顿为目标的老年代回收器。
它的核心思想是:
尽量让垃圾回收和用户线程并发执行,减少停顿时间。
CMS 的主要过程一般包括:
-
初始标记
-
并发标记
-
重新标记
-
并发清除
其中:
-
初始标记和重新标记需要 STW
-
并发标记和并发清除可以和用户线程同时进行
CMS 的优点是:
-
停顿时间短
-
对响应时间敏感的系统比较友好
CMS 的缺点也很典型:
-
会产生内存碎片
-
对 CPU 资源敏感
-
并发阶段如果回收跟不上分配速度,可能触发 Concurrent Mode Failure
-
后期已经被更新的回收器逐步替代
一句话总结:
CMS 的核心价值是低停顿,但它用空间碎片和更复杂的运行成本换来了这个优势。
十六、G1 回收器
G1,Garbage First,是现在非常主流的一款服务端垃圾回收器。
它和传统分代回收器一个很大的区别在于:
G1 不再强调固定连续的新生代、老年代物理边界,而是把整个堆划分成多个大小相等的 Region。
这些 Region 在逻辑上仍然可以扮演 Eden、Survivor、Old 等角色。
G1 的核心特点
1. Region 化
把堆拆成很多小块,而不是简单地分成一整块新生代和一整块老年代。
2. 可预测停顿
G1 会根据回收收益优先选择垃圾最多、回收价值最高的 Region,这也是 "Garbage First" 名字的来源。
3. Mixed GC
G1 不需要每次都 Full GC 才处理老年代,而是可以在回收新生代时顺带挑一些老年代 Region 一起回收。
G1 的适用场景
-
大堆内存
-
多核服务器
-
既希望吞吐不错,又希望停顿尽量可控
G1 的不足
-
实现复杂
-
在某些极端场景下未必总比 CMS 或 Parallel 更有优势
一句话总结:
G1 的核心不是"绝对最快",而是"在大堆场景下尽量把停顿控制得更可预测"。
十七、ZGC 和 Shenandoah 简单了解
如果面试官问到新一代低延迟 GC,可以简单提一下 ZGC 和 Shenandoah。
1. ZGC
ZGC 的目标是把停顿时间压得非常低,哪怕堆很大也尽量保持低停顿。
特点:
-
面向超大堆
-
停顿时间极短
-
更多通过并发阶段完成大部分工作
2. Shenandoah
Shenandoah 也是低停顿回收器,设计目标和 ZGC 类似,也强调尽量并发执行回收过程。
如果只是常规面试,知道到这里通常就够了。除非岗位明确对 JVM 调优要求很高,否则一般不会深挖底层实现细节。
十八、什么是 Stop-The-World
Stop-The-World,简称 STW,指的是:
在 GC 的某些关键阶段,JVM 必须暂停所有用户线程,只让垃圾回收线程工作。
这也是为什么 GC 会影响响应时间的根本原因。
需要注意的是:
-
不是只有 Full GC 才会 STW
-
Minor GC 也可能 STW
-
并发回收器也不是完全不停顿,只是尽量缩短停顿时间
所以判断一个回收器好不好,很多时候看的就是:
-
停顿多久
-
停顿是否可控
-
吞吐量损失大不大
十九、对象一定会被立刻回收吗
不会。
一个对象即使已经不可达,也不代表它会在下一秒立刻被清理。
GC 是按一定时机触发的,具体什么时候回收,要看:
-
当前内存分配情况
-
GC 触发条件
-
使用的回收器
-
JVM 参数配置
所以更准确的理解应该是:
"可回收"不等于"立刻回收",而是说明这个对象已经具备了被 GC 清理的资格。
二十、什么情况下容易频繁 Full GC
这类题既能用于面试,也能用于排查线上问题。
常见原因包括:
1. 老年代空间不足
对象晋升太快,或者老年代回收速度跟不上分配速度,都会导致 Full GC。
2. 大对象分配过多
大对象直接进入老年代,容易快速把老年代打满。
3. 内存泄漏
对象其实已经不该用了,但仍然被引用着,导致 GC 无法回收。
4. 元空间不足
如果频繁动态生成类,比如大量反射、代理、热部署不当,也可能导致方法区相关 Full GC。
5. 参数配置不合理
比如新生代太小、晋升阈值不合适、堆大小配置不合理,也会让 Full GC 更频繁。
二十一、如何理解内存泄漏和内存溢出
这两个概念很容易被混用。
1. 内存泄漏
内存泄漏指的是:
对象本来已经没用了,但仍然被某些引用持有,导致 GC 无法回收。
2. 内存溢出
内存溢出指的是:
JVM 已经没有足够内存完成新的分配请求,最终抛出 OOM。
关系是:
-
内存泄漏不一定立刻 OOM
-
但严重的内存泄漏经常最终会导致内存溢出
二十二、面试里怎么讲"为什么会有分代"
这是一个很值得背的表达。
可以直接这样说:
JVM 之所以采用分代回收,核心原因是不同对象的生命周期差异很大。大多数对象朝生夕死,适合放在新生代,用复制算法快速清理;少数对象长期存活,适合放在老年代,用标记-清除或标记-整理进行回收。这样做能显著提高 GC 效率,避免所有对象都用同一种回收策略。
二十三、面试里怎么讲"G1 和 CMS 的区别"
可以按下面这个思路回答:
CMS 更强调低停顿,主要针对老年代,核心问题是会产生内存碎片,而且在高并发下可能出现并发失败;G1 则把堆拆成多个 Region,能够在回收新生代的同时回收部分老年代,并且更强调停顿时间的可预测性。简单来说,CMS 是传统低停顿方案,G1 是更现代、更通用的大堆低停顿方案。
二十四、面试里怎么讲"GC 算法和回收器的关系"
这也是很容易被问到的一个点。
可以这样理解:
-
GC 算法回答的是"垃圾怎么回收"
-
垃圾回收器回答的是"具体由谁、以什么方式执行这些算法"
比如:
-
Serial、ParNew、G1、CMS 是回收器
-
标记-清除、复制、标记-整理是算法
一句话总结:
算法是理论基础,回收器是工程实现。
二十五、最后给一版适合背诵的总结
如果面试官问"你怎么理解 JVM 的 GC 机制",可以直接按下面这段说:
JVM 的 GC 机制本质上就是自动识别无用对象并回收内存。判断对象是否存活,主流是通过可达性分析,从 GC Roots 出发去查找可达对象。真正执行回收时,底层会用到标记-清除、复制、标记-整理这些算法。由于大多数对象生命周期都很短,所以 JVM 引入了分代回收思想,把堆分成新生代和老年代:新生代对象死亡率高,通常用复制算法;老年代对象存活率高,更适合标记-清除或标记-整理。常见 GC 类型包括 Minor GC、Major GC、Full GC,G1 里还有 Mixed GC。回收器层面,早期有 Serial、ParNew、Parallel、CMS,现在更主流的是 G1,更低停顿场景下还有 ZGC、Shenandoah。理解 GC 的关键,不是死记名字,而是知道每种机制背后在解决什么问题,比如吞吐量、停顿时间、碎片整理和大堆场景下的可控性。