深入理解JVM垃圾回收

JVM垃圾回收(GC,Garbage Collection)是Java自动内存管理的核心机制,它负责识别并回收堆内存中不再被使用的对象,释放内存资源,避免内存泄漏和内存溢出(OOM)。对于Java后端开发者而言,理解GC原理不仅是面试高频考点,更是排查线上性能问题、优化系统吞吐量的关键。

本文将从"垃圾判定→回收算法→垃圾收集器→实战调优"四个维度,系统拆解JVM垃圾回收的核心知识。


一、GC的核心作用与回收范围

1.1 GC的核心目标

Java开发者无需手动调用free()释放内存,GC的核心目标是:

  • 识别堆内存中"死亡"的对象(不再被引用的对象);

  • 安全回收死亡对象占用的内存,重新分配给新对象;

  • 尽可能降低回收过程对应用程序的影响(减少STW时间)。

1.2 GC的回收范围

JVM运行时数据区中,GC主要作用于堆内存(所有对象实例的分配区域),其次是方法区(元空间,仅回收无用类),其他区域(虚拟机栈、本地方法栈、程序计数器)为线程私有,随线程销毁自动释放,不参与GC。

堆内存进一步分为新生代和老年代,不同区域的对象生命周期不同,GC策略也不同:

  • 新生代:对象"朝生夕死",存活率极低(通常不足10%),GC频率高、耗时短;

  • 老年代:对象存活时间长(多次GC后仍存活),存活率高,GC频率低、耗时长。

关键提醒:堆内存是GC的主战场,理解新生代与老年代的划分,是掌握GC算法和收集器的前提。


二、垃圾判定:如何判断对象"已死亡"?

GC的第一步是"识别垃圾"------即判断哪些对象不再被程序使用,目前JVM主流采用可达性分析算法,淘汰了存在致命缺陷的引用计数法。

2.1 被淘汰的方案:引用计数法

原理:为每个对象维护一个引用计数器,当对象被引用时计数器+1,引用失效时计数器-1;当计数器为0时,判定为垃圾。

优点:实现简单、实时性高,无需暂停用户线程。

致命缺陷:无法解决循环引用问题。例如,对象A引用对象B,对象B引用对象A,两者计数器均为1,但外部无任何引用,理论上应被回收,但引用计数法无法识别,会导致内存泄漏。因此,JVM未采用此方案(Python等语言使用该方案,但需额外处理循环引用)。

2.2 JVM主流方案:可达性分析算法

原理:以"GC Roots"为起点,遍历对象的引用链,若一个对象无法通过任何引用链到达GC Roots,则判定为垃圾(不可达对象)。

核心:GC Roots的4类核心来源

  • 虚拟机栈(栈帧局部变量表)中引用的对象(如方法内的局部变量、参数);

  • 方法区中类静态属性引用的对象(如static Object obj = new Object());

  • 方法区中常量引用的对象;

  • 本地方法栈中JNI(本地方法)引用的对象。

对象死亡的"两次标记"机制

不可达对象并非立即被回收,需经历两次标记,确保回收的准确性:

  1. 第一次标记:可达性分析发现对象不可达,标记为"待回收";

  2. 筛选判断:检查该对象是否重写了finalize()方法,且该方法未被执行过;

    • 若未重写或已执行,则直接判定为"死亡",等待回收;

    • 若重写且未执行,则将对象放入F-Queue队列,由Finalizer线程执行finalize()方法;

  3. 第二次标记:执行finalize()后,若对象重新与GC Roots建立引用链("复活"),则移除"待回收"标记;否则,最终判定为"死亡",等待回收。

注意:finalize()方法优先级极低,不保证一定执行,且执行时间不可控,开发中严禁依赖该方法进行资源释放(建议用try-finally替代)。


三、核心垃圾回收算法:GC的"底层实现逻辑"

识别垃圾后,GC需要通过特定算法回收内存,不同算法适用于不同场景(新生代/老年代)。JVM的垃圾回收算法核心有4种,其中分代收集算法是目前主流的组合策略。

3.1 标记-清除算法(Mark-Sweep)

核心原理(两步执行)

  1. 标记阶段:从GC Roots出发,标记所有可达对象;

  2. 清除阶段:遍历整个堆内存,回收所有未标记的垃圾对象,释放内存空间。

优缺点

优点 缺点
实现简单,无需移动对象,执行效率高 1. 内存碎片化严重(回收后产生大量不连续内存块,大对象无法分配时触发Full GC);2. 标记和清除均需遍历全堆,STW时间长

适用场景

仅适用于老年代(对象存活率高,移动成本高),极少单独使用,早期CMS收集器的老年代回收基于此算法优化。

3.2 标记-复制算法(Mark-Copy)

核心原理(空间换时间)

将堆内存划分为两个大小相等的区域(From区和To区),仅使用From区分配对象;GC时执行以下步骤:

  1. 标记阶段:标记From区中所有可达对象;

  2. 复制阶段:将所有标记的存活对象,复制到To区(按内存地址连续排列);

  3. 交换阶段:清空From区,交换From区和To区的角色,下次GC使用新的From区。

优化版:Appel式分配(JVM新生代实际采用)

为解决"内存利用率低"的问题,JVM将新生代划分为1个Eden区 + 2个Survivor区(默认比例8:1:1),而非等大分区:

  • 大部分对象在Eden区创建,GC时标记Eden区+From Survivor区的存活对象,复制到To Survivor区;

  • 若To Survivor区空间不足,存活对象直接晋升到老年代;

  • 交换From和To Survivor区的角色,重复上述过程。

优缺点

优点 缺点
1. 无内存碎片(复制后对象连续排列);2. 回收效率高(仅处理存活对象,新生代存活率低);3. 内存分配时仅需移动指针,效率极高 1. 内存利用率低(需预留To区,优化后仍损失10%新生代空间);2. 老年代对象存活率高,复制成本极高,不适用

适用场景

新生代核心回收算法(所有收集器的新生代回收均基于此),契合新生代对象"朝生夕死"的特点。

3.3 标记-整理算法(Mark-Compact)

核心原理(标记+移动+清除)

  1. 标记阶段:同标记-清除算法,标记所有可达对象;

  2. 整理阶段:将所有标记的存活对象,向堆内存一端压缩移动,保证存活对象连续排列;

  3. 清除阶段:清理内存边界外的所有垃圾对象,释放连续的内存空间。

优缺点

优点 缺点
1. 无内存碎片;2. 内存利用率100%(无需预留空间);3. 适合存活率高的对象 1. 移动对象成本高(需更新所有对象的引用地址);2. 整理过程需暂停所有用户线程,STW时间长

适用场景

老年代核心算法(如Serial Old、Parallel Old收集器),平衡了内存碎片化和内存利用率。

3.4 分代收集算法(Generational Collection)

并非独立算法,而是基于对象生命周期的"组合策略",结合上述三种算法的优势,是目前所有JVM收集器的核心实现逻辑:

  • 新生代:对象存活率低 → 采用标记-复制算法(高效、无碎片);

  • 老年代:对象存活率高、体积大 → 采用标记-清除/标记-整理算法(避免高复制成本);

  • 元空间(方法区):极少回收,仅在无用类卸载时触发(如类加载器被回收、无对象引用该类)。

核心优势:针对不同区域选择最优算法,降低整体GC开销------新生代Minor GC频率高但耗时短,老年代Major GC/Full GC频率低但耗时长,平衡了回收效率和应用性能。


四、主流垃圾收集器:GC的"具体实现"

垃圾回收算法是"底层逻辑",垃圾收集器是"具体实现"。JVM提供了多种收集器,适配不同的应用场景(低延迟、高吞吐量),核心分为三大类:串行收集器、并行收集器、并发收集器。

以下重点讲解目前生产环境中常用的4种收集器,以及已被废弃的CMS收集器(面试高频)。

4.1 串行收集器(Serial GC)

最基础、最简单的收集器,采用"单线程"执行GC,回收期间暂停所有用户线程(STW),新生代采用标记-复制算法,老年代采用标记-整理算法。

特点:实现简单、内存开销小,STW时间长,仅适用于单CPU、小堆内存(<4GB)场景(如桌面应用、嵌入式系统),生产环境(服务器)极少使用。

启用参数:-XX:+UseSerialGC

4.2 并行收集器(Parallel GC)

JDK8默认收集器,又称"吞吐量优先收集器",采用"多线程"执行GC,新生代采用标记-复制算法,老年代采用标记-整理算法。

核心目标:最大化系统吞吐量(吞吐量=应用运行时间/(应用运行时间+GC时间)),通过多线程并行回收,减少GC总耗时。

特点:吞吐量高,STW时间比Serial GC短,适用于高吞吐量需求的场景(如后台任务、数据计算),但STW时间仍不可控,不适合低延迟场景。

启用参数:-XX:+UseParallelGC(新生代并行)、-XX:+UseParallelOldGC(老年代并行)

4.3 并发标记清除收集器(CMS)

一款"低延迟优先"的收集器,核心目标是减少STW时间(控制在100ms内),仅作用于老年代,新生代依赖ParNew收集器(并行标记-复制)。

核心流程(4个阶段)

  1. 初始标记:暂停所有用户线程(STW),标记GC Roots直接引用的对象,耗时极短(毫秒级);

  2. 并发标记:恢复用户线程,GC线程与应用线程并发执行,遍历GC Roots引用链,标记所有可达对象;

  3. 重新标记:暂停所有用户线程(STW),修正并发标记期间因用户线程操作导致的标记偏差,耗时短(十毫秒级);

  4. 并发清除:恢复用户线程,GC线程与应用线程并发执行,回收未标记的垃圾对象。

优缺点

优点 缺点
STW时间短,适合低延迟场景(如电商支付、实时接口);对小堆(2-10GB)友好 1. 内存碎片严重(并发清除不移动对象);2. CPU开销高(并发阶段GC线程与应用线程抢CPU);3. 存在浮动垃圾(并发清除阶段产生的新垃圾,需预留内存);4. 已被废弃(Java 14后移除)

适用场景

仅用于JDK8及以下的旧系统维护(堆2-10GB),新系统不推荐使用,建议迁移至G1收集器。

启用参数:-XX:+UseConcMarkSweepGC

4.4 G1收集器(Garbage-First)

JDK9及以上默认收集器,一款"平衡延迟与吞吐量"的收集器,融合了分代收集和区域化管理的思想,适用于中大型堆内存(4-64GB)。

核心特点

  1. Region化管理:将堆内存划分为多个大小相等的Region(1MB-32MB),每个Region可动态切换为新生代、老年代,无需手动划分区域大小;

  2. 混合回收:优先回收垃圾最多的Region(Garbage-First),兼顾新生代和老年代回收;

  3. 可控停顿:通过停顿预测模型,控制STW时间在目标范围内(默认200ms),平衡延迟与吞吐量。

优缺点

优点 缺点
1. 延迟与吞吐量平衡,适配多数企业级应用(Web服务、电商系统);2. 低内存碎片(筛选回收阶段复制对象);3. 动态适配对象分配特性,无需手动调优;4. 成熟稳定 1. 内存开销较高(记忆集占堆5%-10%);2. 小堆(<4GB)下性价比低;3. 大堆(>64GB)时延迟可能上升

适用场景

中大型堆内存(4-64GB)、需要平衡延迟与吞吐量的场景(如Spring Boot应用、微服务、电商系统),是目前生产环境的首选收集器。

启用参数:-XX:+UseG1GC

4.5 新一代收集器:ZGC

JDK11引入、JDK17稳定的新一代收集器,核心目标是"超低延迟+超大堆",支持最大16TB堆内存,STW时间控制在1ms内,适用于极致低延迟场景(如高频交易、实时数据分析)。

核心优势:全阶段并发(仅初始/最终标记有微秒级STW)、几乎无内存碎片、低CPU开销,但小堆场景性价比低,工具链适配滞后,目前尚未大规模普及。

启用参数:-XX:+UseZGC

4.6 收集器选型建议

  • 小堆(<4GB)+ 吞吐量优先:Serial GC(简单场景)、Parallel GC(性能更优);

  • 中大型堆(4-64GB)+ 平衡需求:G1 GC(首选,默认);

  • 超大堆(≥32GB)+ 超低延迟:ZGC(JDK17+);

  • 旧系统(JDK8)+ 低延迟:暂时保留CMS,建议逐步迁移至G1。


五、GC关键概念与实战调优基础

5.1 核心GC术语

  • STW(Stop The World):GC回收期间,暂停所有用户线程,避免引用链动态变化,是影响应用性能的关键(尽可能缩短STW时间);

  • Minor GC:仅回收新生代的GC,触发条件为Eden区满,频率高、耗时短;

  • Major GC:仅回收老年代的GC,触发条件为老年代空间不足、晋升对象超过老年代剩余空间,频率低、耗时长;

  • Full GC :回收新生代+老年代+元空间的GC,触发条件为Major GC失败、元空间满、显式调用System.gc(),STW时间最长,应尽量避免;

  • 空间分配担保:Minor GC前,JVM检查老年代最大可用连续空间是否≥新生代所有对象总大小,若不足则触发Full GC。

5.2 实战调优核心原则

  1. 优先优化堆内存大小:根据应用场景设置合理的堆内存(-Xms=初始堆大小,-Xmx=最大堆大小,建议两者设置一致,避免频繁扩容);

  2. 选择合适的收集器:结合堆大小、延迟需求选型,避免盲目追新(如小堆用G1反而性能下降);

  3. 减少对象创建频率:避免频繁创建短期对象(如循环内创建字符串),减少Minor GC频率;

  4. 避免内存泄漏:及时释放无用引用(如静态集合持有对象、IO流未关闭),防止老年代溢出;

  5. 监控GC状态:通过JDK自带工具(jstat、jmap、jconsole)监控GC频率、STW时间,针对性调优。

5.3 常用GC监控工具

  • jstat:实时监控GC统计信息(如GC次数、耗时、堆内存使用情况);

  • jmap:生成堆内存快照,分析对象分布、排查内存泄漏;

  • jconsole:图形化工具,直观查看GC状态、堆内存变化、线程状态;

  • VisualVM:功能强大的可视化工具,支持GC分析、内存快照分析、性能采样。


六、面试高频考点总结

  1. GC的核心作用?回收堆内存中死亡的对象,避免内存泄漏和OOM,降低对应用的影响。

  2. 垃圾判定的两种方式?引用计数法(缺陷:循环引用)、可达性分析算法(JVM主流)。

  3. GC Roots包含哪些?虚拟机栈局部变量、方法区静态变量/常量、本地方法栈JNI引用。

  4. 三大基础GC算法的优缺点及适用场景?标记-清除(碎片化)、标记-复制(新生代)、标记-整理(老年代)。

  5. 分代收集算法的核心思想?按对象生命周期分代,新生代用标记-复制,老年代用标记-清除/整理。

  6. CMS和G1的区别?CMS低延迟但有碎片、已废弃;G1平衡延迟与吞吐量,无碎片、目前主流。

  7. 如何减少STW时间?选择合适的收集器、优化堆内存大小、减少对象创建、避免Full GC。


七、总结

JVM垃圾回收的核心是"识别垃圾→回收垃圾→优化回收效率",其演进本质是"延迟、吞吐量、内存开销"的平衡艺术:从Serial GC的简单单线程,到Parallel GC的高吞吐量,再到CMS的低延迟,最后到G1、ZGC的平衡与突破,每一款收集器都对应特定的应用场景。

对于开发者而言,无需深入底层源码实现,重点掌握"垃圾判定原理、核心算法、收集器选型、调优原则",既能应对面试,也能在生产环境中排查GC相关的性能问题,避免内存溢出和系统卡顿,提升应用的稳定性和性能。

相关推荐
Larry_Yanan2 小时前
QML面试常见问题(一)QML中组件呈现方式的方法有哪些
开发语言·c++·qt·ui·面试
RainCity2 小时前
Java Swing 自定义组件库分享(六)
java·笔记·后端
techdashen2 小时前
深入 Rust enum 的内存世界
开发语言·后端·rust
knight_9___2 小时前
大模型project面试7
人工智能·python·算法·面试·大模型·agent
龙码精神2 小时前
TimescaleDB 物联网设备属性历史数据表设计及常用SQL文档
后端
笨蛋不要掉眼泪2 小时前
Java并发编程:线程的创建和运行
java·开发语言·jvm
小小小小宇2 小时前
Go 后端锁机制详解
后端
挖坑的张师傅2 小时前
你的仓库 Agent Ready 了吗?
后端
客场消音器3 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序