JVM 垃圾回收(GC):从标记到回收的完整机制
垃圾回收(Garbage Collection,GC)是 JVM 自动管理内存的核心机制,负责识别并回收不再被使用的对象,释放内存空间。GC 主要作用于堆内存和方法区,其设计直接影响程序的性能和稳定性。本文将全面解析 GC 的核心原理,包括对象存活判断、回收算法、垃圾回收器及实践配置,帮助深入理解 JVM 内存管理机制。
对象存活判断:如何识别 “垃圾”?
GC 的第一步是判断对象是否存活(即是否为 “垃圾”)。JVM 采用两种核心算法:引用计数法和可达性分析。
1. 引用计数法(Reference Counting)
原理:为每个对象维护一个 “引用计数器”,当对象被引用时计数器 + 1,引用失效时 - 1。若计数器为 0,则认为对象是垃圾。
优点:实现简单,判断效率高,回收无延迟。
缺点:
- 需额外存储计数器,增加内存开销;
- 无法解决循环引用问题(如两个对象互相引用,计数器永不为 0,导致无法回收)。
示例:
1
2
3
4
5
6
7
8
9class 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;
// 此时a和b互相引用,计数器均为1,引用计数法无法回收,导致内存泄漏由于循环引用问题,Java 未采用此算法。
2. 可达性分析(Reachability Analysis)
- 原理:以 “根对象集合(GC Roots)” 为起点,通过引用链遍历对象。若对象无法通过任何引用链连接到 GC Roots,则被判定为 “不可达”(垃圾)。
- 解决循环引用:因 GC Roots 不包含堆内对象的引用,循环引用的对象若与 GC Roots 无连接,会被正确标记为垃圾。
(1)GC Roots 的类型
GC Roots 是不在堆中的引用,常见类型包括:
- 虚拟机栈中局部变量表的引用(如方法参数、局部变量);
- 本地方法栈中 JNI(Native 方法)的引用;
- 方法区中类静态属性的引用(如
static Object obj); - 方法区中常量的引用(如字符串常量池的引用);
- 被
synchronized持有的对象引用; - JVM 内部引用(如基本类型的
Class对象、系统类加载器)。
(2)对象的三种状态
通过可达性分析,对象会处于以下状态:
- 可触及的:与 GC Roots 有直接或间接引用链;
- 可复活的:无引用链,但未执行
finalize()方法(可能在该方法中重新建立引用链); - 不可触及的:
finalize()方法已执行且未复活,确定为垃圾。
(3)两次标记过程(最终判定垃圾)
对象需经历两次标记才会被回收:
- 第一次标记:对象与 GC Roots 无引用链,进入 “可复活” 状态;
- 筛选:判断是否需执行
finalize()方法(未重写或已执行过则无需执行); - 第二次标记:若需执行
finalize(),对象被放入F-Queue队列,由Finalizer线程执行该方法;若方法中未重新建立引用链,则被标记为 “不可触及”(垃圾)。
注意:
finalize()仅执行一次,且不建议依赖(执行时机不确定,可能导致对象复活干扰 GC)。
垃圾回收算法:如何回收 “垃圾”?
确定垃圾后,JVM 需通过回收算法释放内存。常见算法包括标记 - 清除、复制、标记 - 整理,以及基于它们的分代收集和分区算法。
1. 标记 - 清除算法(Mark-Sweep)
- 步骤:
- 标记:从 GC Roots 遍历,标记所有存活对象;
- 清除:遍历堆,回收未标记的垃圾对象,释放内存。
- 优点:实现简单,无需移动对象。
- 缺点:
- 效率低(标记和清除均需全堆遍历);
- 产生内存碎片(回收后空闲内存不连续,大对象可能无法分配)。
2. 复制算法(Copying)
步骤:
- 将堆分为两块等大区域(如 From 区和 To 区),仅使用其中一块;
- 回收时,将存活对象复制到未使用的区域,清除原区域所有对象;
- 交换两块区域的角色(From 变 To,To 变 From)。
优点:
- 效率高(仅复制存活对象);
- 无内存碎片(复制后内存连续)。
缺点:
- 需双倍内存空间(浪费 50% 内存);
- 不适合存活对象多的场景(复制成本高)。
适用场景:新生代(存活对象少,垃圾多)。
3. 标记 - 整理算法(Mark-Compact)
步骤:
- 标记:同标记 - 清除,标记存活对象;
- 整理:将所有存活对象移动到堆的一端,按顺序排列,然后清除边界外的垃圾。
优点:
- 无内存碎片(整理后内存连续);
- 无需双倍内存(优于复制算法)。
缺点:
- 效率低(标记 + 移动对象,成本高);
- 移动对象时需调整引用地址,且需 STW(Stop The World)。
适用场景:老年代(存活对象多,垃圾少)。
4. 分代收集算法(Generational Collection)
分代收集是组合上述算法的复合策略,基于 “对象存活时间不同” 的观察:
新生代:对象存活时间短(如临时变量),采用复制算法(高效处理低存活率对象);
老年代:对象存活时间长(如缓存对象),采用标记 - 清除或标记 - 整理算法(避免复制算法的内存浪费)。
工作流程:
- 新对象优先分配在新生代 Eden 区;
- Eden 区满时触发 Minor GC(新生代 GC),存活对象移至 Survivor 区;
- 对象在 Survivor 区经历多次 GC 后,晋升至老年代;
- 老年代满时触发 Major GC 或 Full GC(老年代 GC)。
5. 分区算法(Region-Based)
将堆划分为多个独立的小区域(Region),每个区域可单独回收。优点是:
控制单次 GC 回收的区域数量,减少 STW 时间;
适合大堆场景(如 10GB 以上),避免全堆扫描。
代表:G1 收集器(基于分区算法)。
垃圾回收器:算法的具体实现
垃圾回收器是 GC 算法的具体实现,JVM 提供多种回收器,适配不同场景(如低延迟、高吞吐量)。
1. 按线程模式分类
- 串行回收器:单线程执行 GC,STW 时间长,适合单核环境。
- 并行回收器:多线程执行 GC,提高吞吐量,STW 时间中等。
- 并发回收器:GC 线程与用户线程并发执行,降低 STW 时间,适合低延迟场景。
2. 经典垃圾回收器对比
| 回收器 | 分代 | 算法 | 线程模式 | STW 情况 | 核心特点 | 适用场景 |
|---|---|---|---|---|---|---|
| Serial | 新生代 | 复制 | 串行 | 长(单线程) | 简单高效,适合小堆、Client 模式 | 单核环境、简单应用 |
| Serial Old | 老年代 | 标记 - 整理 | 串行 | 长(单线程) | Serial 老年代版本,作为 CMS 后备 | 与 Serial/Parallel 配合 |
| ParNew | 新生代 | 复制 | 并行 | 中(多线程) | Serial 的并行版,可与 CMS 配合 | 多核环境、需低延迟 |
| Parallel Scavenge | 新生代 | 复制 | 并行 | 中(多线程) | 吞吐量优先,支持自适应调节 | 后台任务、批处理(高吞吐量) |
| Parallel Old | 老年代 | 标记 - 整理 | 并行 | 中(多线程) | Parallel Scavenge 的老年代版本 | 与 Parallel Scavenge 配合 |
| CMS | 老年代 | 标记 - 清除 | 并发 + 并行 | 短(仅初始 / 重新标记) | 低延迟,并发回收 | 交互式应用(如 Web 服务) |
| G1 | 整堆(分区) | 复制 + 标记 - 整理 | 并发 + 并行 | 可控(按优先级回收) | 大堆友好,可预测停顿,无内存碎片 | 大堆应用(>4GB,如大型服务) |
3. 重点回收器详解
(1)CMS 收集器(低延迟优先)
CMS(Concurrent Mark Sweep)是老年代的并发回收器,目标是减少 STW 时间:
步骤:
- 初始标记:标记 GC Roots 直接引用的对象(STW,耗时短);
- 并发标记:遍历引用链,标记存活对象(与用户线程并发,无 STW);
- 重新标记:修正并发标记期间的对象状态变化(STW,耗时短);
- 并发清理:回收垃圾对象(与用户线程并发,无 STW)。
优点:并发回收,STW 时间短(低延迟)。
缺点:
- 产生内存碎片(标记 - 清除算法);
- 占用 CPU 资源(并发阶段与用户线程竞争);
- 无法处理 “浮动垃圾”(并发清理时产生的新垃圾,需下次 GC 回收)。
配置参数:
1
2
3-XX:+UseConcMarkSweepGC # 启用 CMS
-XX:+UseParNewGC # 新生代配合 ParNew(默认开启)
-XX:CMSInitiatingOccupancyFraction=70 # 老年代占用70%时触发GC
(2)G1 收集器(大堆 + 可预测延迟)
G1(Garbage-First)是面向大堆的分区回收器,兼顾吞吐量和延迟:
核心设计:将堆分为 2048 个 Region(1MB~32MB),每个 Region 可动态标记为 Eden、Survivor 或老年代。
步骤:
- 新生代 GC:回收 Eden 和 Survivor 区,存活对象移至其他 Region;
- 并发标记:标记全堆存活对象(与用户线程并发);
- 混合回收(Mixed GC):优先回收 “价值最高” 的 Region(回收收益 / 时间比最高),兼顾 Eden 和部分老年代。
优点:
- 无内存碎片(Region 间复制 + 整理);
- 可预测停顿(通过
-XX:MaxGCPauseMillis控制); - 适合大堆(支持 TB 级内存)。
配置参数:
1
2
3-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认200ms)
-XX:G1HeapRegionSize=8m # 每个 Region 大小(1M~32M,2的次幂)
STW 事件(Stop The World)
STW 是 GC 过程中所有用户线程暂停的现象,目的是确保 GC 能在一致的内存快照中进行(避免对象引用动态变化导致标记错误)。
- 触发场景:可达性分析的根节点枚举、CMS 的初始标记 / 重新标记、G1 的新生代 GC / 混合回收等。
- 影响:STW 时间过长会导致应用卡顿(如 Web 服务响应延迟),需通过选择合适的回收器和参数优化。
GC 类型与触发条件
根据回收范围,GC 分为部分收集和整堆收集:
| 类型 | 回收范围 | 触发条件 |
|---|---|---|
| Minor GC | 新生代(Eden + Survivor) | Eden 区满时触发 |
| Major GC | 老年代 | 老年代空间不足(仅 CMS 支持单独回收) |
| Mixed GC | 新生代 + 部分老年代 | G1 中老年代占用达到阈值(默认 45%) |
| Full GC | 全堆 + 方法区 | 老年代不足 / 方法区不足 / 显式调用 System.gc() |
GC 性能评估与参数配置
1. 核心评估指标
- 吞吐量:用户代码执行时间 /(用户代码时间 + GC 时间),越高越好(适合后台任务)。
- 停顿时间:STW 持续时间,越短越好(适合交互式应用)。
- 内存占用:堆内存使用量,需平衡吞吐量和延迟。
2. 常用 GC 日志参数
通过日志分析 GC 行为,优化参数配置:
1 | -XX:+PrintGC # 输出简单 GC 日志 |
分析工具:GCViewer、GCEasy(在线工具:gceasy.io)。
3. 回收器选择建议
- 小堆(<4GB)+ 低延迟:ParNew + CMS
- 大堆(>4GB)+ 可预测延迟:G1
- 高吞吐量:Parallel Scavenge + Parallel Old
- 单核环境:Serial + Serial Old