JVM垃圾回收机制超全详解:从底层原理到线上调优实战
一、前言:为什么要学JVM垃圾回收?
1.1 开发痛点
在 Java 开发中,你是否遇到过这些头疼的问题?
- 内存溢出(OOM):应用突然崩溃,日志抛出
java.lang.OutOfMemoryError。 - 程序卡顿:接口响应时间突然变长,用户投诉不断。
- 线上GC雪崩:频繁 Full GC 导致 CPU 飙升,服务假死,整个集群响应超时。
这些问题的根源,往往都指向一个核心机制——垃圾回收(Garbage Collection,GC)。
1.2 GC的核心价值
C/C++ 中,开发者需要手动 malloc/free 或 new/delete,一旦忘记释放就会内存泄漏,释放过早又会出现野指针。JVM 的 自动内存管理 将开发者从繁重且易错的内存管理中解放出来,让我们能更专注于业务逻辑。
1.3 学习定位
无论你是准备面试、进行性能调优,还是想深入理解 Java 底层,GC 都是必须攻克的重难点。它是高级开发的必备技能,也是大厂面试的高频考察领域。
1.4 全文学习预告
本文将带你从 底层原理 出发,吃透 回收算法,对比 主流收集器,最后落到 实战调优 和 面试避坑。让我们用一张全景图开启学习:
二、GC核心基础:先搞懂垃圾回收是什么
2.1 什么是JVM垃圾回收?
定义:JVM 自动识别堆内存中不再被使用的对象(无效对象),并释放其占用内存的机制。
核心目标:回收无效内存、避免内存泄漏、保证内存高效利用。
专注区域:GC 主要针对 堆内存(新生代、老年代),而栈内存随着方法结束自动释放,无需 GC 介入。
2.2 核心前提:如何判断对象是“垃圾”?
1. 引用计数算法
为每个对象维护一个引用计数器,被引用时+1,引用失效时-1,计数为0即可回收。
- 优点:实现简单,判定效率高。
- 致命缺陷:无法解决 循环引用 问题(A引用B,B引用A,但两者都已无外部引用)。
class A { B b; }
class B { A a; }
A a = new A(); B b = new B();
a.b = b; b.a = a;
a = null; b = null; // 循环引用,引用计数不为0,永远无法回收
2. 可达性分析算法(JVM主流算法)
核心原理:以一系列称为 GC Roots 的对象为起点,通过引用链向下搜索,走过的路径称为 引用链。若某个对象到任何 GC Roots 都不可达,则判定为可回收对象。
四大GC Roots核心对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:当前正在执行的方法中的局部变量。
- 方法区中类静态属性引用的对象:如
static修饰的引用类型变量。 - 方法区中常量引用的对象:如字符串常量池中的引用。
- 本地方法栈中 JNI(Native方法)引用的对象。
不可达对象不一定会立即被回收,至少需要经历 两次标记过程:第一次标记并筛选是否需要执行 finalize(),若有必要则放入 F-Queue 队列,由 Finalizer 线程执行;第二次标记若仍不可达,则真正回收。
2.3 Java引用体系(GC回收的关键依据)
JDK 1.2 后,Java 将引用分为四种,强度依次递减:
| 引用类型 | 回收时机 | 使用场景 |
|---|---|---|
| 强引用(Strong Reference) | 永不回收,OOM也不收 | 普通 Object obj = new Object() |
| 软引用(Soft Reference) | 内存不足时回收 | 内存敏感的高速缓存 |
| 弱引用(Weak Reference) | 下次GC必回收 | WeakHashMap、ThreadLocal |
| 虚引用(Phantom Reference) | 无法通过它获取对象,仅用于回收通知 | 管理堆外内存(DirectByteBuffer) |
2.4 对象存活与终结:finalize机制
finalize() 是对象被回收前执行的最后方法,但不推荐使用:
- 执行不确定:JVM 不保证立即执行,甚至可能不执行。
- 性能差:会拖慢 GC 速度。
- 无法兜底:关键资源释放应使用
try-finally或try-with-resources。
三、JVM内存分代模型:GC分代回收的底层逻辑
3.1 堆内存分代划分
现代 JVM 将堆内存划分为不同区域,便于高效回收:
┌────────────────────────────────────┐
│ 堆内存 (Heap) │
│ ┌───────────┬──────────┬────────┐ │
│ │ 新生代 │ 老年代 │ 元空间 │ │
│ │ (Young) │ (Old) │(Metaspace)│
│ │ │ │ JDK8+ │ │
│ └───────────┴──────────┴────────┘ │
│ 新生代进一步划分: │
│ ┌────┬──────┬──────┐ │
│ │Eden│S0 │S1 │ │
│ │ │(From)│(To) │ │
│ └────┴──────┴──────┘ │
└────────────────────────────────────┘
- 新生代:存放新创建的对象,由 Eden 和两个 Survivor(S0/S1)组成,比例为默认
8:1:1。 - 老年代:存放长期存活的对象(经历多次GC仍存活)或大对象。
- 元空间(JDK8+):存放类元数据,使用本地内存,替代永久代。
3.2 分代回收核心思想
基于两大假说:
- 弱分代假说:绝大多数对象都是朝生夕灭,生命周期很短。
- 强分代假说:熬过越多次 GC 的对象越难以被回收。
因此,JVM 对新生代进行 高频、快速 的轻量级 GC,对老年代进行 低频、重量级 的 GC。
3.3 对象晋升机制
对象从新生代晋升到老年代遵循以下规则:
- 年龄阈值:对象每熬过一次 Minor GC 年龄+1,默认达到 15 岁(可通过
-XX:MaxTenuringThreshold调整)晋升老年代。 - 大对象直接进入老年代:可通过
-XX:PretenureSizeThreshold设置阈值,避免大对象在 Eden 区和 Survivor 区之间来回复制。 - 动态年龄判定:若 Survivor 区中 相同年龄 的所有对象大小总和超过 Survivor 空间的一半,则年龄大于等于该年龄的对象可直接晋升。
四、四大经典GC回收算法(核心原理详解)
4.1 标记-清除算法(Mark-Sweep)
流程:先标记所有需要回收的对象,再统一清除。
优点:简单。
缺点:
- 内存碎片:清除后产生大量不连续碎片,可能导致分配大对象时无法找到连续空间而触发 GC。
- 效率低:标记和清除过程效率都不高。
适用场景:老年代基础回收(但一般不单独使用)。
4.2 复制算法(Copying)
流程:将可用内存分为大小相等的两块,每次只使用其中一块。GC时将存活对象复制到另一块,然后清理当前块。
优点:无内存碎片,回收效率高。
缺点:内存利用率只有 50%,浪费空间。
新生代 Eden : Survivor 比例 8:1:1 的奥秘:只有 10% 的内存被浪费(每次一个 Survivor 区空闲),因为新生代 98% 的对象熬不过一次 GC,复制成本极低。
4.3 标记-整理算法(Mark-Compact)
流程:先标记存活对象,然后让所有存活对象向内存一端移动,最后清理掉边界以外的内存。
优点:无内存碎片,内存利用率高。
缺点:整理过程耗时较长,尤其是老年代存活对象多时,STW 时间显著。
适用场景:老年代(对象存活率高)。
4.4 分代收集算法
核心思路:不同代区适配不同算法。
- 新生代:对象死得多,用 复制算法。
- 老年代:对象存活率高,用 标记-清除 + 标记-整理 的组合(CMS 用标记-清除,Serial Old/Parallel Old 用标记-整理)。
五、GC分类与核心概念(必懂术语)
5.1 按回收区域分类
| GC类型 | 回收区域 | 特点 | 触发时机 |
|---|---|---|---|
| Minor GC | 仅新生代 | 频率高、速度快、STW短 | Eden区满时触发 |
| Major GC | 仅老年代 | 频率低、速度慢、STW长 | 通常与Full GC混淆,CMS中特指老年代GC |
| Full GC | 整堆(新生+老年代+元空间) | 开销最大,STW最长 | 老年代满、元空间满、System.gc()等 |
5.2 核心关键术语
- STW(Stop-The-World):GC 时所有应用线程暂停,直到 GC 线程完成。Minor GC 的 STW 通常在毫秒级,Full GC 可达秒级。优化的核心就是 减少 STW 时间。
- 内存泄漏 vs 内存溢出:
- 内存泄漏:对象不再使用,但 GC Roots 仍有引用链,无法回收(如静态集合不断添加未清理元素)。
- 内存溢出:内存不够用了,通常由内存泄漏或大对象分配导致。
六、主流JVM垃圾收集器详解(从经典到前沿)
6.1 串行收集器(Serial / Serial Old)
- Serial:新生代,单线程,复制算法。
- Serial Old:老年代,单线程,标记-整理算法。
- 特点:客户端模式默认,简单高效,单核CPU下无线程交互开销。但多核场景下停顿时间长。
- 启用参数:
-XX:+UseSerialGC
6.2 并行收集器(吞吐量优先)
- ParNew:新生代,Serial 的多线程版本,复制算法。可与 CMS 搭配。
- Parallel Scavenge / Parallel Old:新生代+老年代多线程组合,追求 可控吞吐量(
吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC时间))。 - 核心参数:
-XX:MaxGCPauseMillis:最大停顿时间-XX:GCTimeRatio:吞吐量占比(1-99)
- 适用场景:后台运算型任务,对停顿不敏感,追求高吞吐。
6.3 并发收集器:CMS(低延迟优先)
CMS(Concurrent Mark Sweep)以 最短停顿时间 为目标,基于标记-清除算法。执行分为四步:
- 初始标记(STW):仅标记 GC Roots 直接关联对象,很快。
- 并发标记:与用户线程并发,遍历引用链。
- 重新标记(STW):修正并发标记期间变动的对象标记,比初始标记稍长但远小于并发标记。
- 并发清除:与用户线程并发,清理垃圾。
优点:低停顿。
致命问题:
- 内存碎片:标记-清除算法固有缺点,可能触发 Full GC(Serial Old 整理)。
- 浮动垃圾:并发清除时用户线程可能产生新垃圾,只能下次回收。
- 并发失败:预留内存不够时,会退化为 Serial Old 停顿式回收。
启用参数:-XX:+UseConcMarkSweepGC
6.4 新一代高性能收集器(重点)
G1收集器(JDK9默认)
核心思想:不再严格分代,而是将堆划分为多个大小相等的 Region,每个 Region 可扮演 Eden、Survivor 或 Old 角色。通过 可预测的停顿模型,优先回收价值最大的 Region(Garbage First)。
- 工作流程:初始标记 → 并发标记 → 最终标记 → 筛选回收(STW,通过复制算法将选中 Region 的存活对象移至空闲 Region)。
- 核心优势:停顿时间可配置(
-XX:MaxGCPauseMillis),兼顾吞吐量与低延迟,适合大堆(6GB+)。
ZGC收集器(JDK11+)
目标:亚毫秒级停顿(<1ms),支持 TB 级堆。基于 染色指针 和 读屏障 实现并发整理,停顿时间不随堆大小增长。JDK15 起正式生产可用。
Shenandoah收集器
RedHat 开发的低停顿收集器,与 ZGC 类似,但使用 转发指针 和 Brooks Pointer 实现并发整理。
6.5 主流收集器对比选型
默认收集器变化:
- JDK8:Parallel Scavenge + Parallel Old
- JDK9+:G1
- JDK15+:ZGC 生产就绪
七、GC实战调优核心内容
7.1 常用JVM GC核心参数
# 堆内存
-Xms4g -Xmx4g # 初始堆/最大堆一致,避免动态扩容
-Xmn2g # 新生代大小
-XX:SurvivorRatio=8 # Eden : Survivor = 8:1
# 收集器选择
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:G1HeapRegionSize=4m # G1 Region 大小
# GC日志(JDK8+)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log
# OOM时Dump堆快照
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/dump.hprof
7.2 常见GC问题排查
1. 频繁Minor GC
- 现象:GC日志中 Minor GC 间隔几秒甚至更短。
- 排查:查看年轻代大小是否过小,或系统瞬间产生大量短命对象(如接口超时后大量请求积压)。
- 解决:适当增大年轻代、优化代码减少对象创建。
2. Full GC频繁/时间过长
- 现象:
[Full GC ...]日志频现,STW 超过 1 秒。 - 排查:
- 是否存在 内存泄漏(dump 堆分析,查看静态集合、ThreadLocal 未清理等)。
- 元空间设置过小(
-XX:MaxMetaspaceSize),频繁触发 Full GC 回收类。 - CMS 并发失败退化,考虑更换 G1。
- 解决:修复泄漏点、扩大元空间、更换收集器。
3. OOM排查思路
- 堆溢出:
java.lang.OutOfMemoryError: Java heap space→ 查看大对象或泄漏。 - 元空间溢出:
OutOfMemoryError: Metaspace→ 检查类加载是否异常(如动态代理无限生成类)。
7.3 线上GC调优通用流程
八、高频面试题总结(避坑+标准答案)
1. 可达性分析算法的GC Roots包含哪些?
- 虚拟机栈中引用的对象
- 静态属性引用的对象
- 常量引用的对象
- 本地方法栈中JNI引用的对象
2. 四种引用的区别和使用场景?
| 引用类型 | 回收时机 | 典型场景 |
|---|---|---|
| 强引用 | 永不 | 普通对象 |
| 软引用 | 内存不足 | 缓存 |
| 弱引用 | 下次GC | WeakHashMap, ThreadLocal |
| 虚引用 | 随时 | 堆外内存回收通知 |
3. 四大GC算法的优缺点对比?
- 标记-清除:实现简单,有碎片。
- 复制:无碎片,效率高,浪费一半空间,适合存活率低的场景。
- 标记-整理:无碎片,但整理耗时。
- 分代收集:组合拳,新生代用复制,老年代用整理/清除。
4. CMS、G1、ZGC的核心区别与选型?
- CMS:并发清除,低延迟,但有碎片,大堆表现差。
- G1:Region 化,可预测停顿,适合 6GB+ 堆。
- ZGC:亚毫秒级,TB 堆支持,JDK15+ 成熟。
5. Minor GC和Full GC的触发条件?
- Minor GC:Eden 区满。
- Full GC:老年代满、元空间满、显式调用
System.gc()、CMS 并发失败等。
6. STW是什么?哪些GC阶段会产生STW?
STW 是应用线程全部暂停等待 GC。所有收集器的 标记阶段 和 复制/整理阶段 都可能产生 STW,只是时间长短不同。即使是并发收集器,初始标记、重新标记等阶段也需要短暂 STW。
7. 内存泄漏和内存溢出的区别与解决方案?
- 泄漏:对象无法回收,用 MAT/JProfiler 分析 GC Roots 引用链,修复代码。
- 溢出:内存不足,可增大堆内存或排查泄漏/大对象。
九、总结
JVM 垃圾回收从 判断垃圾(可达性分析)开始,依据 分代假说 将堆划分为新生代、老年代,分别采用 复制、标记-整理 等算法,通过 各类收集器(Serial → Parallel → CMS → G1 → ZGC)实现自动化回收。线上调优时,需结合 GC日志 与 监控数据,从内存分配、收集器选型、代码层面综合优化。
不同业务场景的优化重心不同:
- 高吞吐:后台计算任务,选 Parallel 收集器。
- 低延迟:交互式应用,G1 或 ZGC。
- 小内存/单核:Serial 简单可靠。
后续进阶可深入研究 GC 源码实现、JVM 调优实战案例 以及 云原生场景下的新收集器演进。
参考与推荐阅读
- 《深入理解Java虚拟机》周志明
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)