1. 完整回答
JVM 垃圾回收,也叫 GC(Garbage Collection),是 JVM 自动管理内存的一种机制,主要负责识别并回收堆中不再使用的对象,减少手动释放内存带来的问题。
垃圾回收主要解决两个问题:哪些对象可以回收,以及怎样高效地回收。
首先,JVM 主要通过可达性分析判断对象是否存活。从 GC Roots 出发,沿着引用关系查找对象,无法从 GC Roots 到达的对象就可能被回收。
确定回收对象后,垃圾收集器会采用标记-清除、复制或者标记-整理等算法释放内存。
为了提高效率,主流垃圾收集器还利用分代回收思想。因为大多数对象生命周期比较短,所以堆通常按对象生命周期划分为新生代和老年代。新生代主要回收短生命周期对象,老年代主要管理存活时间较长的对象,减少每次回收需要处理的数据量。
在实际运行中,GC 还需要平衡三个指标:吞吐量、停顿时间和内存占用。例如,G1 将堆划分为多个 Region,通过分批回收控制停顿;ZGC 则让大部分回收工作与应用线程并发执行,进一步降低停顿时间。
2. 核心原理与关键概念
2.1 核心原理
JVM 垃圾回收可以分成四个主要环节。
第一步:分配对象
对象通常优先在堆的新生代 Eden 区分配。为了减少多线程分配时的竞争,HotSpot 通常使用线程本地分配缓冲区(TLAB)加速对象分配。
第二步:判断对象存活
以 GC Roots 为起点进行可达性分析:
GC Roots → 遍历对象引用链 → 标记存活对象 → 确定可回收对象
GC Roots 常见来源包括线程栈中的活动引用、静态字段关联的引用,以及 JNI 引用等。
第三步:执行垃圾回收
以传统分代模型为例:
新对象进入 Eden → 新生代空间不足触发 Young GC → 存活对象复制到 Survivor 区或晋升老年代 → 释放原区域空间
对于老年代,收集器根据自身机制完成标记和空间回收。例如 G1 会通过并发标记识别老年代中的存活对象,再通过 Mixed GC 分批回收部分老年代 Region。
第四步:整理空间并继续分配
采用复制或整理算法的收集器会移动存活对象,并更新相关引用,使回收后的空间能够继续分配对象。
不同收集器的执行流程存在差异,例如 ZGC 可以并发完成对象转移,而 G1 的主要对象转移阶段需要暂停应用线程。
2.2 关键概念
| 概念 | 含义 |
|---|---|
| GC Roots | 可达性分析的起点,用于判断对象是否仍然可访问 |
| Eden 区 | 新生代中主要用于新对象分配的区域 |
| Survivor 区 | 保存经过新生代回收后仍然存活的对象 |
| 老年代 | 主要保存生命周期较长的对象 |
| 对象晋升 | 对象经过一定次数的回收或满足其他条件后进入老年代 |
| STW | Stop-The-World,GC 暂停应用线程的过程 |
| 吞吐量 | 应用有效运行时间占总运行时间的比例 |
| 停顿时间 | GC 导致应用线程暂停的持续时间 |
其中,Eden、Survivor 和老年代描述的是典型的分代回收模型。G1 使用 Region 实现逻辑分代,不要求这些区域在物理上连续。
3. 高频面试追问
3.1 JVM 如何判断一个对象是否可以被回收?
主要通过可达性分析算法判断对象是否存活。
JVM 会从 GC Roots 出发,沿着对象之间的引用关系进行搜索。能够被访问到的对象通常认为仍然存活,无法访问到的对象就可能被垃圾回收器回收。
这里需要关注对象是否还能通过 GC Roots 访问到。
例如,两个对象 A 和 B 相互引用,但已经没有任何 GC Roots 能够访问它们,那么这两个对象仍然可以被回收。
另外,还有一种引用计数法,通过记录对象被引用的次数判断是否存活,但单纯的引用计数无法处理循环引用,因此 HotSpot 主要使用可达性分析。
继续追问:GC Roots 通常包括哪些对象?
主要包括线程栈中活动的对象引用、通过静态字段访问的对象、JNI 持有的对象,以及 JVM 内部维护的部分引用。
这些引用构成可达性分析的起点,可以帮助垃圾收集器识别仍然需要保留的对象。
3.2 JVM 为什么要采用分代垃圾回收?对象如何进入老年代?
分代回收主要是为了利用不同对象的生命周期特点,提高垃圾回收效率。
大多数 Java 对象生命周期比较短,比如一次 HTTP 请求中创建的临时对象,通常请求处理完成后就不再使用。只有少部分对象会长期存活,比如应用缓存、单例对象等。
如果每次 GC 都扫描整个堆,开销会比较大。
因此,分代收集器把堆逻辑上划分为新生代和老年代,优先回收垃圾比例较高的新生代,老年代则采用适合长期存活对象的回收策略。
对象进入老年代的典型流程是:
Eden 创建对象 → Young GC 后存活 → 进入 Survivor → 经历多次回收 → 满足晋升条件 → 进入老年代
另外,当 Survivor 空间不足时,部分对象也可能提前晋升,具体行为取决于垃圾收集器。
继续追问:新生代为什么通常使用复制算法?
因为新生代大部分对象很快就会死亡,每次真正需要复制的存活对象比较少。
复制算法只需要把存活对象移动到其他区域,就能直接回收原区域,效率比较高,也能减少内存碎片。
3.3 常见的垃圾回收算法有哪些?各有什么特点?
常见的基础垃圾回收算法有三种:标记-清除、复制和标记-整理。
标记-清除算法会先标记存活对象,再清理未存活对象。实现相对简单,但清除后可能产生不连续的空闲空间,也就是内存碎片。
复制算法会把存活对象复制到其他区域,然后整体释放原来的区域。它的优点是分配方便、碎片少,适合存活对象比例较低的区域;缺点是需要额外的目标空间,并且复制存活对象存在开销。
标记-整理算法会先标记存活对象,再把存活对象向一侧移动,最后清理边界之外的空间。它能减少内存碎片,但移动大量存活对象的成本较高。
在传统分代垃圾回收模型中:
-
新生代一般采用复制算法。因为新生代大部分对象生命周期较短,每次 GC 后存活的对象比较少,只需要复制少量存活对象,回收效率较高。
-
老年代一般采用标记-清除或标记-整理算法。因为老年代对象存活率较高,如果使用复制算法,需要移动大量存活对象,开销比较大。其中,Serial GC 和 Parallel GC 的老年代回收主要采用标记-整理,早期 CMS 主要采用标记-清除。
不过,具体算法还取决于垃圾收集器。例如,G1 的新生代和老年代都可以通过 Region 之间的对象复制转移完成空间回收,同时实现内存整理。
3.4 Minor GC、Major GC、Full GC 和 Mixed GC 有什么区别?
主要区别在于回收范围和具体的收集器行为。
Minor GC 通常指新生代回收,也叫 Young GC。由于新生代大部分对象生命周期短,所以回收频率比较高,通常耗时较短。
Major GC 通常指涉及老年代的回收,但这个名称在不同垃圾收集器中没有完全统一的含义。
Full GC 通常涉及整个 Java 堆的回收,可能同时进行类卸载等工作。对于 G1,Full GC 是代价较高的兜底回收方式,会产生较长的 STW 停顿。
Mixed GC 是 G1 中的混合回收,一次回收会同时处理新生代 Region 和部分老年代 Region,可以分批回收老年代。
因此,观察 GC 日志时,需要结合实际使用的收集器判断回收范围和停顿原因。
继续追问:Young GC 一定比 Full GC 快吗?
通常 Young GC 更快,因为处理的区域较小,而且大部分对象已经死亡。
但具体耗时还取决于存活对象数量、引用扫描开销和系统负载。新生代存活对象很多时,Young GC 也可能出现较长停顿。
3.5 G1 垃圾收集器的核心原理是什么?
G1 的核心思想是把堆划分成多个 Region,通过预测回收成本,分批选择回收价值较高的区域,尽量控制停顿时间。
传统分代收集器通常使用相对连续的新生代和老年代布局,而 G1 使用大小相等的 Region。每个 Region 可以根据需要承担 Eden、Survivor 或老年代区域的角色。
G1 的正常回收主要分为两个阶段。
第一阶段是年轻代回收阶段,主要通过 Young GC 回收新生代对象。
当老年代占用达到一定条件后,会启动并发标记,统计老年代 Region 中的存活对象情况。
第二阶段是空间回收阶段,通过 Mixed GC 将新生代和部分有回收价值的老年代 Region 一起回收。
整体流程可以概括为:
Young GC → 启动并发标记 → 识别老年代回收候选 Region → Mixed GC 分批回收 → 进入下一轮回收周期
G1 还有两个重要机制:
-
记忆集(Remembered Set):记录回收区域外部指向回收区域的引用位置,减少跨 Region 引用扫描的成本。
-
SATB 快照标记:在并发标记期间维护一个逻辑上的初始对象图快照,避免应用线程修改引用时遗漏需要保留的对象。
G1 适合需要兼顾吞吐量和停顿时间的服务,但停顿目标并不能保证每次都严格达到。
继续追问:为什么 G1 仍然可能发生 Full GC?
当内存回收速度跟不上对象分配速度,或者对象转移时缺少足够的目标空间,G1 就可能进入 Full GC 兜底回收。
这种回收需要处理整个堆并整理存活对象,因此可能产生明显的长时间停顿。
3.6 什么是 STW?并行回收和并发回收有什么区别?
STW 指垃圾回收过程中,JVM 暂停应用线程执行的现象。
GC 需要准确识别对象引用关系,有些阶段还需要移动对象并更新引用。如果应用线程同时修改这些数据,就会增加回收过程的复杂度。
因此,HotSpot 会在部分回收阶段让应用线程进入安全点,暂停应用线程,保证相关操作能够安全执行。
另外,并行回收和并发回收是两个不同概念。
并行回收指多个 GC 线程同时执行垃圾回收工作,可以缩短回收阶段的执行时间,但应用线程可能仍处于暂停状态。
并发回收指 GC 线程与应用线程同时运行,可以减少应用暂停时间,但需要额外的同步机制,也会占用一定 CPU 资源。
例如,G1 的并发标记可以与应用线程一起执行,但 Young GC 和 Mixed GC 中的主要对象转移阶段仍然需要 STW。
ZGC 则把更多耗时操作放到并发阶段,因此通常能实现更短的 GC 停顿。
3.7 常见的垃圾收集器有哪些?实际项目中如何选择?
常见的 HotSpot 垃圾收集器包括 Serial GC、Parallel GC、G1 和 ZGC,选择时主要看吞吐量、停顿要求和资源开销。
Serial GC 使用单线程完成垃圾回收,适合堆比较小、资源受限的应用。
Parallel GC 使用多个线程并行执行垃圾回收,目标是提高整体吞吐量,适合批处理、后台计算等能够接受较长停顿的场景。
G1 通过 Region、并发标记和分批回收来控制停顿,是现代 HotSpot 中常见的默认垃圾收集器,比较适合大多数 Java 服务。
ZGC 则以低停顿为主要目标,通过并发标记、并发对象转移,以及染色指针和访问屏障等机制,尽量减少应用线程暂停时间,适合对响应延迟特别敏感的系统。
需要注意,ZGC 从 JDK 24 开始已经只保留分代模式,不能再按照早期非分代 ZGC 的实现来理解。
实际选择时,可以先使用 G1 观察吞吐量和响应时间。如果 GC 停顿明显影响请求尾延迟,再评估 ZGC;如果主要追求批处理吞吐量,也可以测试 Parallel GC。
3.8 强引用、软引用、弱引用和虚引用有什么区别?
四种引用的主要区别是对对象可达性和垃圾回收行为的影响不同。
强引用就是普通对象引用。只要对象仍然能够通过强引用从 GC Roots 到达,垃圾收集器就会保留它。
软引用通过 SoftReference 实现。软可达对象在内存充足时通常可以保留,在内存压力增大时可能被清理,适合某些对内存敏感、可以重新生成的数据。
弱引用通过 WeakReference 实现。对象只剩弱引用时,垃圾收集器在发现它弱可达后就可以清除弱引用,常见应用包括 WeakHashMap。
虚引用通过 PhantomReference 实现,无法通过虚引用直接获取对象,通常配合 ReferenceQueue 感知对象可达性变化,辅助完成资源清理等操作。
实际项目中,大部分业务对象使用强引用。弱引用主要用于不希望阻止对象回收的场景,而软引用缓存需要考虑清理时机不确定的问题。
3.9 Java 有 GC,为什么还会发生内存泄漏?
因为 GC 只能回收满足回收条件的对象。如果不再使用的对象仍然被强引用持有,就可能发生内存泄漏。
例如,一个静态 Map 不断存储请求数据,却没有及时删除。即使这些数据已经失去业务价值,由于静态 Map 仍然可以从 GC Roots 访问,垃圾收集器也无法回收它们。
类似问题还包括缓存没有淘汰策略、监听器没有注销、线程池中的 ThreadLocal 没有及时清理等。
这种情况下,堆内存会持续增长,GC 后能够释放的空间越来越少,最终可能出现频繁 GC 或 OutOfMemoryError。
排查时可以生成 Heap Dump,通过 MAT 等工具分析对象的保留内存和 GC Roots 引用链,找出哪些对象被意外持有。
因此,自动垃圾回收能够降低内存管理成本,但业务代码仍然需要正确管理对象生命周期。
3.10 线上出现频繁 Full GC,应该怎样排查和优化?
首先需要确认 GC 的触发原因,再判断是内存配置、对象分配速度还是内存泄漏导致的问题。
一般按照以下流程排查:
第一步:查看 GC 日志。
可以通过 -Xlog:gc* 记录 GC 信息,分析垃圾回收的频率、停顿时间、触发原因,以及回收前后的堆占用情况。
第二步:观察内存变化。
如果每次 GC 后堆占用都能明显下降,但很快又被填满,可能是短时间内创建了大量对象,或者堆容量不足。
如果多次 GC 后,老年代占用持续升高,能够回收的空间越来越少,就需要重点检查长期存活对象和潜在内存泄漏。
第三步:分析对象分配与引用关系。
可以使用 JFR 观察对象分配情况,结合 jcmd、Heap Dump 和 MAT 分析哪些对象占用大量内存,以及这些对象为什么仍然存活。
第四步:针对原因优化。
如果是大量临时对象,可以优化对象创建逻辑;如果是缓存持续增长,需要设置合理的缓存容量和淘汰机制;如果堆容量确实不足,可以根据实际内存需求调整 -Xmx。
如果使用 G1,还需要关注对象晋升速度、并发标记是否及时完成,以及是否出现对象转移失败。
总体上,应该先定位异常对象和实际内存压力,再决定是否调整 GC 参数。
继续追问:频繁 Full GC 就一定是内存泄漏吗?
不一定。
堆容量过小、对象分配速度过快、突发流量、老年代存活对象过多,都可能造成频繁 Full GC。
判断是否存在内存泄漏,需要重点观察 GC 后的存活对象占用是否持续增长,并结合堆转储中的引用关系确认。
4. 易混淆概念对比
4.1 可达性分析与引用计数
| 对比项 | 可达性分析 | 引用计数 |
|---|---|---|
| 判断依据 | 对象能否从 GC Roots 到达 | 对象被引用的次数 |
| 循环引用 | 能识别不可达的循环引用对象 | 单独使用时无法解决循环引用 |
| 核心特点 | 需要遍历对象引用关系 | 需要维护引用次数 |
| JVM 应用 | HotSpot 主要采用 | HotSpot 未将其作为主要存活判断机制 |
4.2 三种垃圾回收算法
| 算法 | 核心机制 | 优点 | 局限性 |
|---|---|---|---|
| 标记-清除 | 标记存活对象,清理垃圾 | 实现相对简单 | 容易产生内存碎片 |
| 复制 | 复制存活对象,释放原区域 | 分配方便,碎片少 | 需要目标空间,存在复制开销 |
| 标记-整理 | 标记存活对象并移动整理 | 减少内存碎片 | 对象移动和引用更新成本较高 |
4.3 Young GC、Mixed GC 与 Full GC
| 类型 | 回收范围 | 主要特点 |
|---|---|---|
| Young GC | 新生代 | 通常频繁、停顿较短 |
| Mixed GC | G1 的新生代和部分老年代 Region | 分批回收部分老年代 |
| Full GC | 通常涉及整个 Java 堆 | 回收范围大,往往停顿较长 |
Major GC 通常描述老年代回收,但具体含义依赖收集器和日志语境。
4.4 并行回收与并发回收
| 对比项 | 并行回收 | 并发回收 |
|---|---|---|
| 执行方式 | 多个 GC 线程同时工作 | GC 线程与应用线程同时工作 |
| 主要目标 | 缩短 GC 工作耗时 | 减少应用暂停时间 |
| 应用线程 | 可能处于 STW 状态 | 在并发阶段继续运行 |
| 典型应用 | Parallel GC | G1 的并发标记、ZGC 的并发回收 |
4.5 Parallel GC、G1 与 ZGC
| 对比项 | Parallel GC | G1 | ZGC |
|---|---|---|---|
| 优化目标 | 高吞吐量 | 吞吐量与停顿平衡 | 极低停顿 |
| 核心机制 | 多线程并行回收 | Region 分区、并发标记、分批转移 | 并发标记与并发对象转移 |
| 停顿特点 | 可能较长 | 尽量满足停顿目标 | 通常非常短 |
| 主要代价 | 长停顿影响响应时间 | 记忆集维护、并发标记等开销 | 并发工作、屏障和内存余量开销 |
| 适用场景 | 批处理、后台计算 | 常规 Java 服务 | 对尾延迟敏感的服务 |
5. 总结
JVM 垃圾回收主要负责自动识别和回收堆中不再使用的对象。核心机制是通过 GC Roots 可达性分析判断对象存活,再结合复制、标记-清除和标记-整理等算法回收内存。主流收集器通常利用分代思想提高效率,同时通过并行和并发机制平衡吞吐量与停顿时间。G1 通过 Region 和分批回收控制停顿,ZGC 通过高度并发的回收机制进一步降低延迟。实际应用中,GC 可以自动管理对象内存,但无法避免被意外引用导致的内存泄漏,因此还需要结合 GC 日志、对象分配情况和堆内存分析定位性能问题。