JVM 垃圾回收(GC)核心原理全解析(HotSpot 视角,JDK 8~21 适用)
垃圾回收(Garbage Collection,简称 GC)是 JVM 内存管理的核心机制,帮助程序员自动回收无用对象内存,避免内存泄漏。JVM 的 GC 基于分代假设:大多数对象朝生夕死,少数对象长寿。
本文从垃圾判断、回收算法、分代收集、常见收集器、STW 与优化一步步解析原理,到实际调优建议。基于 HotSpot VM(最主流实现),适用于 JDK 8+(包括 ZGC 等新收集器)。
一、GC 基础:为什么需要 GC?什么是垃圾?
- 为什么需要 GC?
C/C++ 需要手动 malloc/free,容易内存泄漏或悬垂指针。Java 通过 GC 自动管理堆内存,让开发者专注业务。 - 什么是垃圾?
堆上对象如果不再被任何线程引用,就是垃圾。GC 先判断哪些是垃圾,再回收。
二、垃圾判断算法(如何判定对象已死?)
两大主流算法:引用计数 vs 可达性分析(JVM 用后者)。
| 算法 | 原理 | 优点 | 缺点 | JVM 使用? |
|---|---|---|---|---|
| 引用计数 | 对象有一个计数器,引用+1,失效-1;计数=0 时回收 | 简单、实时回收 | 无法解决循环引用(A 引用 B,B 引用 A) | 否 |
| 可达性分析 | 从 GC Roots 开始搜索,可达的对象是活的;不可达的是垃圾 | 解决循环引用 | 需要 STW(暂停用户线程) | 是(主流) |
GC Roots 是什么?(搜索起点)
- 虚拟机栈中的局部变量引用
- 方法区静态变量引用
- 方法区常量引用
- 本地方法栈 JNI 引用
- 线程对象(Thread)
- 已加载类对象(Class)
- 监视器(synchronized 持有的对象)
对象“死亡”过程(不是立即回收):
- 不可达 → 标记为垃圾
- 如果重写了 finalize(),放入 F-Queue 队列,自救机会(finalize() 中重新建立引用)
- finalize() 只执行一次,自救失败 → 真正回收
注意:finalize() 已被弃用(JDK9+),建议用 try-with-resources 或 Cleaner。
三、垃圾回收算法(如何回收垃圾?)
三大经典算法 + 分代优化。
- 标记-清除(Mark-Sweep)
- 过程:标记活对象 → 清除垃圾
- 优点:简单
- 缺点:碎片化(空间不连续,导致大对象分配失败);效率低(标记+清除两次扫描)
- 适用:老年代(CMS 使用)
- 标记-复制(Mark-Copy)
- 过程:标记活对象 → 复制到另一块空间 → 清空原空间
- 优点:无碎片;简单高效(只扫描活对象)
- 缺点:空间浪费(一半空间空闲)
- 适用:新生代(存活率低,复制少量对象)
- 标记-整理(Mark-Compact)
- 过程:标记活对象 → 整理(活对象移到一端) → 清空剩余
- 优点:无碎片;空间利用高
- 缺点:整理阶段慢(移动对象,更新引用)
- 适用:老年代(Serial Old、Parallel Old 使用)
算法对比表
| 算法 | 空间利用 | 时间效率 | 无碎片? | 典型收集器 |
|---|---|---|---|---|
| 标记-清除 | 高 | 中等 | 否 | CMS |
| 标记-复制 | 低 | 高 | 是 | Serial、ParNew |
| 标记-整理 | 高 | 低 | 是 | Serial Old、G1(局部) |
四、分代收集理论(Generational Collection)
基于弱代假设(大多数对象短命)和强代假设(越老越难死),堆分为新生代和老年代。
- 新生代(Young Gen):Eden + 两个 Survivor(默认 8:1:1)
- 新对象在 Eden 分配
- Minor GC(Young GC):回收新生代(复制算法)
- 存活对象 → Survivor(年龄+1)
- 年龄达阈值(默认15,-XX:MaxTenuringThreshold)或 Survivor 满 → 晋升老年代
- 老年代(Old Gen):存长寿对象
- Major GC / Full GC:回收老年代 + 新生代(标记-清除/整理)
- 触发:老年代满、方法区满、担保失败
- 担保机制(Allocation Failure Guarantee):
新生代 Minor GC 前检查老年代空间是否够容纳所有新生代对象(极端情况)。不够 → Full GC。
分代比例:新生代:老年代 = 1:2(-XX:NewRatio=2,默认)
五、常见 GC 收集器(Collector)及原理
HotSpot 有多种收集器,按新生代/老年代分类,按串行/并行/并发分类。
新生代收集器:
- Serial:单线程、复制算法、STW
- ParNew:Serial 多线程版、并行
- Parallel Scavenge:并行、吞吐量优先(-XX:MaxGCPauseMillis、-XX:GCTimeRatio)
老年代收集器:
- Serial Old:单线程、标记-整理
- Parallel Old:Parallel Scavenge 的老年代版、并行
- CMS(Concurrent Mark Sweep):并发标记-清除,低暂停
- 过程:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除
- 优点:低延迟(并发阶段用户线程可运行)
- 缺点:碎片、CPU 敏感、浮动垃圾(并发时新垃圾)
全堆收集器(JDK8+ 主流):
- G1(Garbage First):分代 + 区域(Region)
- 堆分成小 Region(1-32MB),优先回收价值高的(垃圾多)
- 过程:Young GC(复制)+ Mixed GC(年轻+老)
- 优点:可预测暂停(-XX:MaxGCPauseMillis=200ms,默认)
- 缺点:高内存占用(Remembered Set 跟踪跨代引用)
- 适用:大堆(>4GB)、低延迟
- ZGC(JDK11+):超低暂停(<10ms)
- 使用彩色指针(Colored Pointers)标记,不需 STW 整理
- 优点:暂停极短、支持 TB 级堆
- 缺点:吞吐量稍低
- Shenandoah(JDK12+,OpenJDK):类似 ZGC,并发整理
收集器组合推荐(生产常用):
- 小堆、低延迟:ParNew + CMS
- 高吞吐:Parallel Scavenge + Parallel Old
- 大堆、均衡:G1(JDK9+ 默认)
- 超大堆、低暂停:ZGC / Shenandoah
参数示例:
# G1 示例
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx4g -Xms4g
六、STW(Stop The World)与优化
- STW 是什么?:GC 时暂停所有用户线程,确保引用一致。
- 为什么需要?:防止标记时引用变化。
- 优化:并发收集器(CMS/G1/ZGC)把标记/清除并发化,只短暂停。
安全点(Safepoint):线程可暂停的位置(方法调用、循环末尾)。
安全区域:线程 sleep/block 时,整个区域安全。
七、GC 触发时机 & 日志分析
- Minor GC:Eden 满
- Full GC:老年代满、方法区满、System.gc()(不推荐)
日志参数:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
示例日志:
[GC (Allocation Failure) [PSYoungGen: 2048K->256K(2560K)] 2048K->256K(9728K), 0.001s]
- PSYoungGen:新生代
- Allocation Failure:分配失败触发
八、快速记忆口诀 & 调优建议
- 判断:可达分析 > 引用计数(循环问题)
- 算法:清除碎片多、复制空间半、整理移动慢
- 分代:年轻复制快、老年清除/整理
- 收集器:Serial 单、Par 多、CMS 并发碎片、G1 区域预测、ZGC 彩色低停
生产调优:
- 监控 GC 日志(jstat、jmap、VisualVM)
- 堆大小:-Xms = -Xmx(避免动态扩展)
- 新生代大小:-XX:NewSize(太大 Minor GC 少但久,小太频繁)
- 优先 G1/ZGC
- 避免大对象、内存泄漏(ThreadLocal 未 remove)
GC 是 JVM 的黑魔法,原理懂了,调优就简单了。有具体 GC 日志想分析、或某个收集器想深挖(如 G1 的 RSet),告诉我,我继续展开!