一、前言:为什么要学JVM垃圾回收?

1.1 开发痛点

在 Java 开发中,你是否遇到过这些头疼的问题?

  • 内存溢出(OOM):应用突然崩溃,日志抛出 java.lang.OutOfMemoryError
  • 程序卡顿:接口响应时间突然变长,用户投诉不断。
  • 线上GC雪崩:频繁 Full GC 导致 CPU 飙升,服务假死,整个集群响应超时。

这些问题的根源,往往都指向一个核心机制——垃圾回收(Garbage Collection,GC)

1.2 GC的核心价值

C/C++ 中,开发者需要手动 malloc/freenew/delete,一旦忘记释放就会内存泄漏,释放过早又会出现野指针。JVM 的 自动内存管理 将开发者从繁重且易错的内存管理中解放出来,让我们能更专注于业务逻辑。

1.3 学习定位

无论你是准备面试、进行性能调优,还是想深入理解 Java 底层,GC 都是必须攻克的重难点。它是高级开发的必备技能,也是大厂面试的高频考察领域。

1.4 全文学习预告

本文将带你从 底层原理 出发,吃透 回收算法,对比 主流收集器,最后落到 实战调优面试避坑。让我们用一张全景图开启学习:

垃圾判定
可达性分析

分代模型
新生代/老年代

回收算法
标记-清除/复制/整理

垃圾收集器
Serial/Parallel/CMS/G1/ZGC

实战调优
参数/日志/问题排查


二、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

存活对象A

存活对象B

存活对象C

存活对象D

可回收对象E

可回收对象F

四大GC Roots核心对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象:当前正在执行的方法中的局部变量。
  2. 方法区中类静态属性引用的对象:如 static 修饰的引用类型变量。
  3. 方法区中常量引用的对象:如字符串常量池中的引用。
  4. 本地方法栈中 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-finallytry-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 对象晋升机制

对象从新生代晋升到老年代遵循以下规则:

  1. 年龄阈值:对象每熬过一次 Minor GC 年龄+1,默认达到 15 岁(可通过 -XX:MaxTenuringThreshold 调整)晋升老年代。
  2. 大对象直接进入老年代:可通过 -XX:PretenureSizeThreshold 设置阈值,避免大对象在 Eden 区和 Survivor 区之间来回复制。
  3. 动态年龄判定:若 Survivor 区中 相同年龄 的所有对象大小总和超过 Survivor 空间的一半,则年龄大于等于该年龄的对象可直接晋升。

四、四大经典GC回收算法(核心原理详解)

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

流程:先标记所有需要回收的对象,再统一清除。
优点:简单。
缺点

  • 内存碎片:清除后产生大量不连续碎片,可能导致分配大对象时无法找到连续空间而触发 GC。
  • 效率低:标记和清除过程效率都不高。

回收后

存活

空闲

存活

空闲

回收前

存活

垃圾

存活

垃圾

适用场景:老年代基础回收(但一般不单独使用)。

4.2 复制算法(Copying)

流程:将可用内存分为大小相等的两块,每次只使用其中一块。GC时将存活对象复制到另一块,然后清理当前块。
优点:无内存碎片,回收效率高。
缺点:内存利用率只有 50%,浪费空间。

新生代 Eden : Survivor 比例 8:1:1 的奥秘:只有 10% 的内存被浪费(每次一个 Survivor 区空闲),因为新生代 98% 的对象熬不过一次 GC,复制成本极低。

GC

Eden + S0 使用中

复制存活对象到 S1

清空 Eden + S0

交换 S0/S1 角色

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)以 最短停顿时间 为目标,基于标记-清除算法。执行分为四步:

  1. 初始标记(STW):仅标记 GC Roots 直接关联对象,很快。
  2. 并发标记:与用户线程并发,遍历引用链。
  3. 重新标记(STW):修正并发标记期间变动的对象标记,比初始标记稍长但远小于并发标记。
  4. 并发清除:与用户线程并发,清理垃圾。

优点:低停顿。
致命问题

  • 内存碎片:标记-清除算法固有缺点,可能触发 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 主流收集器对比选型

JDK11+,大堆

JDK8/9,6GB+

小堆,JDK8-

否,高吞吐量优先

客户端/单核

选择收集器

追求低延迟?

堆大小与JDK版本

ZGC / Shenandoah

G1

CMS(或Parallel)

Parallel Scavenge + Parallel Old

Serial + Serial Old

默认收集器变化

  • 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日志与监控

2. 分析问题:停顿长 / 频率高 / OOM

3. 定位根因:内存泄漏/参数不当/收集器不合适

4. 调整参数:内存大小/收集器/停顿目标

5. 压测验证 + 灰度发布

6. 持续监控,迭代优化


八、高频面试题总结(避坑+标准答案)

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虚拟机》周志明
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐