JVM 垃圾回收机制深度解析(含图解)
一、垃圾回收整体流程
垃圾回收图解
二、垃圾对象判定算法
以下是对 JVM 垃圾回收机制的进一步细化,结合图解直观展示核心概念、算法流程及收集器工作原理:
2.1 引用计数法(Reference Counting)
原理:
-
计数器管理
- 每个对象内嵌一个引用计数器(Reference Counter)。
- 引用增加(如被赋值、作为参数传递) → 计数器+1。
- 引用失效(如变量置空、离开作用域) → 计数器-1。
-
回收条件
- 当计数器归零时,对象立即被标记为垃圾并回收。
循环引用问题: 无法处理循环引用(如对象 A 和 B 互相引用),导致内存泄漏。
场景描述:
对象A和B互相引用,导致两者的引用计数始终≥1,即使它们已不被其他对象引用,也无法被回收。
后果:内存泄漏(Memory Leak)。
解决循环引用的常见方法
-
弱引用(Weak Reference)
弱引用不增加对象的引用计数(如Java的WeakReference、Python的weakref)。
-
手动断开引用
开发者需在代码中显式解除循环引用(易出错,不推荐)。
-
周期检测算法
定期运行垃圾回收器检测循环引用(如Python的gc模块)。
2.2 可达性分析(Reachability Analysis)
可达性分析示意图
原理:
从一组称为GC Roots的根对象出发,沿着引用链向下搜索,所有能被访问到的对象视为“存活”,未被访问的则判定为“不可达”对象,可被回收。
整体结构
外层是可达性分析的主流程,内嵌三色标记法的核心流程。
颜色标识统一:
白色:未处理或可回收对象(最终状态)
灰色:待处理对象(中间状态)
黑色 :已处理完成对象
红色边框:垃圾回收相关操作
关键流程节点
-
标记GC Roots: 从GC Roots(根对象如栈、静态变量等)出发,递归遍历所有可达对象,将其直接引用的堆对象标记为灰色,并加入灰色队列,作为标记的起点。
-
三色标记循环: 通过灰色队列迭代处理对象(白→灰→黑状态变化)。
- 取出灰色对象,遍历其引用的子对象;
- 若子对象为白色,标记为灰色并加入队列;
- 当前灰色对象标记为黑色;
- 重复直到灰色队列为空。”
-
回收阶段: 清除所有白色(未被标记/不可达)对象,释放内存。
颜色状态映射
存活对象标记流程:
GC Roots → 白色 → 灰色 → 遍历引用 → 黑色(完成)
垃圾对象:
始终为白色 → 最终被回收
可达性分析的优势
-
解决循环引用问题
即使对象A和对象B互相引用,若它们无法从GC Roots到达,仍会被判定为垃圾。 -
高效性
通过引用链遍历,仅需处理存活对象,避免全堆扫描。
并发标记的挑战
在并发标记阶段(如CMS、G1回收器),用户线程可能修改对象引用,导致两种问题:
-
漏标(Missing Mark)
-
增量更新(Incremental Update)
若黑色对象新增对白色对象的引用,将该黑色对象重新标记为灰色(需重新扫描)。
-
原始快照(SATB)
记录标记开始时的对象引用关系,后续新增的引用关系视为“待处理”。
-
-
多标(Floating Garbage)
- 已标记为存活的对象被用户线程置为不可达(通常容忍到下次GC处理)。
GC Roots 包括:
1. 虚拟机栈(Java 栈)中的引用对象
来源: 方法执行时,栈帧中局部变量表存储的引用类型变量。
生命周期: 与方法调用绑定,方法结束后引用失效。
示例:
public void method() {
Object obj = new Object(); // obj 是 GC Root
// 方法执行期间,obj 引用的对象不会被回收
} // 方法结束后,obj 出栈,对象失去 GC Root 引用
2. 方法区中静态属性引用的对象
来源: 类的静态变量(static 字段)。
生命周期: 与类的生命周期相同,类卸载前始终存活。
示例:
public class MyClass {
static Object staticObj = new Object(); // staticObj 是 GC Root
}
3. 方法区中常量引用的对象
来源: 字符串常量池(String Table)、类常量(final static)等。
示例:
public class Constants {
public static final String NAME = "Doubao"; // "Doubao" 是 GC Root
public static final List<String> LIST = Collections.unmodifiableList(
Arrays.asList("a", "b") // 列表对象是 GC Root
);
}
4. 本地方法栈中 JNI 引用的对象
来源: Java 调用本地代码(如 C/C++)时,本地方法栈中保存的引用。
示例:
public native void nativeMethod(); // 本地方法可能持有对象引用,成为 GC Root
5. 其他特殊引用
- 活动线程(Active Threads):
Thread thread = new Thread(() -> {
Object obj = new Object(); // obj 被线程栈引用,成为 GC Root
// ...
});
thread.start();
- 类加载器(ClassLoader):
加载的类和静态变量。
- JVM 内部对象:
如系统类(java.lang.Class)、异常对象(ThreadDeath)等。
GC Roots 内存示意图
三、垃圾回收算法详解
3.1 标记 - 清除(Mark-Sweep)
步骤:
1. 标记: 在堆中遍历所有对象,找出内存中需要回收的对象,并且把它们标记出来。
(蓝色为存活对象、灰色为可回收对象、白色为可用内存)
2. 清除: 清除掉被标记需要回收的对象,释放出对应的内存空间。
缺点:
- 效率低: 两次遍历堆,耗时较长。
- 内存碎片: 回收后产生不连续内存块,可能导致大对象分配失败。
3.2 复制(Copying)
步骤:
- 将内存分为Eden 区和两个Survivor 区(通常比例 8:1:1)。
- 新对象分配到 Eden 区,当 Eden 区满时触发 Minor GC。
- 存活对象复制到 From Survivor (s0) 区,清空 Eden 区。
- 下次 GC 时,将 Eden+ From Survivor (s0) 的存活对象复制到 To Survivor (s1) 区,清空原区域。
- 经历多次 GC 仍存活的对象晋升到老年代。
图解:
- 将内存划分为两块相等的区域,每次只使用其中一块
(蓝色为存活对象、灰色为可回收对象、白色为可用内存、绿色为保留内存)
3. 当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
优点: 无内存碎片,效率高(只需移动指针)。
缺点: 浪费 50% 内存空间(实际采用 8:1:1 比例,仅浪费 10%)。
3.3 标记 - 整理(Mark-Compact)
步骤:
- 标记: 遍历标记存活对象(灰色)。
- 整理: 将存活对象向一端移动,直接清理边界外的内存(白色区域)。
- 优点: 无内存碎片,适合对象存活率高的场景(如老年代)。
图解:
- 标记过程仍然与 标记-清除算法 一样,但是后续步骤不是直接对可回收对象进行清理,
而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
(蓝色为存活对象、灰色为可回收对象、白色为可用内存)
- 让所有存活的对象都向一端移动,清理掉边界以外的内存。
缺点: 需移动对象,成本较高。
3.4 分代收集(Generational Collection)
原理:
根据对象存活周期将堆分为新生代和老年代:
- 新生代(Eden+Survivor): 对象存活率低,采用 复制算法。
- 老年代: 对象存活率高,采用 标记 - 清除 或 标记 - 整理算法 。
- 永久代 / 元空间: 存储类信息、常量池等,GC 频率低。
四、垃圾收集器详解
以下是 Java 主流垃圾回收器的核心启动参数及典型配置
按 分代收集器 和 分区收集器 分类整理:
I、分代收集器
4.1 Serial 收集器
特点: 单线程,STW(Stop The World)【全程暂停用户应用程序】,采用复制算法。
算法:
-
新生代:复制算法(Copying)
将存活对象从Eden区和Survivor区复制到另一个Survivor区,清除未存活对象。 -
老年代:标记-整理算法(Mark-Compact)
标记存活对象后,整理内存空间,消除碎片。
适用场景:
单线程环境(Client 模式),客户端模式或小内存应用。
参数:
# 新生代和老年代均使用Serial(单线程)
-XX:+UseSerialGC -Xmx512m
4.2 Parallel Scavenge 收集器(吞吐量优先)
特点: 多线程,关注吞吐量(运行用户代码时间 / 总时间)。
算法:
-
新生代(Parallel Scavenge):复制算法
多线程并行复制存活对象,提升回收效率。 -
老年代(Parallel Old):标记-整理算法
多线程并行标记和整理,减少碎片。
适用场景:
多核服务器,注重吞吐量而非低延迟。
参数:
# 新生代Parallel Scavenge + 老年代Parallel Old
-XX:+UseParallelGC -XX:+UseParallelOldGC -Xmx4g
# 控制最大停顿时间(毫秒)
-XX:MaxGCPauseMillis=100
# 控制吞吐量目标(默认99%)(如 99 表示 1% 时间用于 GC)。
-XX:GCTimeRatio=99
# 自适应调整内存区域大小
-XX:+UseAdaptiveSizePolicy
4.3 CMS(Concurrent Mark Sweep)收集器(低延迟)
算法:
- 标记-清除算法(Mark-Sweep)
- 工作流程:
1. 初始标记(STW,短暂):标记 GC Roots 直接关联的对象。
2. 并发标记:与用户线程并发执行,遍历可达对象。
3. 重新标记(STW):修正并发标记阶段因用户线程运行导致的对象引用变化(使用增量更新算法)。
4. 并发清除:与用户线程并发未标记对象。
缺点:
-
内存碎片: 标记-清除算法不整理内存,长期运行后碎片化严重,可能导致:
-
晋升失败(Promotion Failed): 新生代对象无法晋升到老年代。
-
大对象分配失败(Humongous Allocation Failure)。
-
最终触发 Full GC,使用 Serial Old(标记-整理算法) 压缩内存。
CMS的核心目标是通过标记-清除算法减少垃圾回收的停顿时间(STW)。 然而,由于其设计上的限制,CMS在某些场景下触发Full GC, 无法通过并发操作完成垃圾回收,此时JVM会触发故障回退机制(Fallback Mechanism), 强制使用单线程的Serial Old收集器(标记-整理算法) 进行压缩执行Full GC以恢复内存可用性。 这种设计是CMS在低延迟与内存连续性之间的权衡。
-
-
吞吐量下降: 并发阶段占用 CPU 资源,与用户线程竞争。
-
无法处理突发分配压力: 并发周期中若老年代空间不足,触发并发模式失败(Concurrent Mode Failure)。
触发Full GC的场景 | 描述 | 是否使用Serial Old |
---|---|---|
并发模式失败 | 并发标记期间老年代空间不足 | 是 |
晋升失败 | 新生代对象晋升到老年代时因碎片无法分配 | 是 |
显式调用System.gc() | 强制触发Full GC(默认开启ExplicitGCInvokesConcurrent可绕过此行为) | 是 |
元空间耗尽 | 类元数据区无法扩展 | 是 |
其他内存分配失败 | 直接内存(Direct Memory)或本地内存(Native Memory)耗尽等 | 是 |
适用场景:
Web 服务器、需要快速响应的应用、老年代垃圾回收、追求低停顿时间。
参数:
# 新生代ParNew + 老年代CMS
-XX:+UseConcMarkSweepGC -Xmx8g
# 老年代使用70%时触发CMS(默认92%)
-XX:CMSInitiatingOccupancyFraction=70
# 控制Full GC时是否进行内存压缩(减少碎片)(标记-整理)。
-XX:+UseCMSCompactAtFullCollection
# 并行执行重新标记阶段
-XX:+CMSParallelRemarkEnabled
# 指定经过多少次Full GC后执行一次压缩(0表示每次Full GC都压缩)。
-XX:CMSFullGCsBeforeCompaction=0
II、分区收集器
4.4 G1(Garbage-First)收集器(区域化、平衡延迟与吞吐量)
算法:
-
分区(Region)
将堆划分为多个大小相等的 Region(如 2MB-32MB)。 -
新生代:复制算法
存活对象复制到Survivor Region或晋升到老年代Region。 -
老年代:标记-整理算法
并发标记后,优先回收垃圾最多的Region(Garbage-First),并整理Region内空间。 -
动态分代
每个 Region 可动态扮演 Eden、Survivor、Old 或 Humongous(大对象)。 -
可预测停顿
根据 Region 的回收价值(垃圾占比)排序,优先回收收益高的区域。 -
工作流程
1. 初始标记(STW 极短): 标记 GC Roots 直接关联的 Region。
2. 并发标记: 与用户线程并发标记存活对象。
3. 最终标记(STW): 处理并发阶段的增量更新(使用 SATB 快照算法)。
4. 筛选回收(STW): 根据停顿时间目标选择 Region 进行回收,采用复制算法。
关键说明:
G1 设计上通过 Mixed GC(混合回收) 避免 Full GC,但在极端情况(如并发标记失败)下仍可能触发 Full GC,此时 触发故障回退机制(Fallback Mechanism) 到单线程的 Serial Old收集器(标记-整理算法)。
- 触发条件: 堆内存不足且无法通过 Mixed GC 回收足够空间。
- 优化建议: 通过 -XX:G1ReservePercent 增加预留内存,降低 Full GC 概率。
适用场景:
大堆内存(数十GB至数百GB)、追求低延迟与吞吐量平衡的应用。
参数:
# 启用G1收集器(JDK 9+默认)
-XX:+UseG1GC -Xmx16g
# 设置最大停顿时间目标(毫秒)
-XX:MaxGCPauseMillis=200
# 设置Java堆区域大小(默认2的幂,1-32MB)
-XX:G1HeapRegionSize=8m
# 老年代占用50%时启动混合回收
-XX:InitiatingHeapOccupancyPercent=50
# G1 垃圾收集器预留堆内存的百分比,默认值:10(即预留 10% 的堆内存)
-XX:G1ReservePercent=10
4.5 ZGC收集器(Z Garbage Collector)(超低延迟,JDK 11+)
特点:
-
着色指针(Colored Pointers)
将 GC 信息(Marked0/1、Remapped)存储在指针的低 4 位,无需扫描对象头。 -
读屏障(Load Barrier)
在读取对象引用时动态修正指针,实现并发移动。 -
并发整理
全程几乎无 STW(仅初始标记和再标记有极短停顿,通常 < 1ms)。
适用场景:
超大堆内存(TB 级),追求极致低延迟(<10ms),如金融交易系统。
算法:
-
并发标记-整理算法(Concurrent Mark-Compact)
1. 并发标记: 利用染色指针(Colored Pointers)标记对象状态。
2. 并发预备重分配: 确定需要清理的内存区域。
3. 并发重分配: 将存活对象复制到新地址,并更新引用。
4. 并发重映射: 修复旧地址到新地址的映射。
5. 关键技术: 染色指针和读屏障,支持全阶段并发执行。
参数:
# 启用ZGC(需JDK 11+)
-XX:+UseZGC -Xmx32g
# 设置最大堆大小(ZGC适合大内存)
-Xmx64g -Xms64g
# 设置并发GC线程数(默认CPU核心数的1/4)
-XX:ConcGCThreads=4
# 配置GC周期的最大停顿时间(目标值)
-XX:MaxGCPauseMillis=10
4.6 Shenandoah收集器(超低延迟,OpenJDK 12+)
特点:
- 与 ZGC 类似,基于转发指针和布鲁姆过滤器实现并发回收,停顿时间极短。
适用场景:
OpenJDK 环境下的超大堆内存(TB级)、极低停顿时间(亚毫秒级)、低延迟应用。
算法:
-
并发复制算法(Concurrent Copying)
1. 并发标记: 与用户线程并发标记存活对象。
2. 并发预清理: 确定需要回收的Region。
3. 并发转移: 在用户线程运行时,将存活对象复制到新Region。
4. 引用更新: 并发更新对象引用至新地址。
5. 关键技术: 使用 读屏障(Read Barrier) 实现并发对象移动。
参数:
# 启用Shenandoah(需JDK 12+,OpenJDK默认包含)
-XX:+UseShenandoahGC -Xmx16g
# 设置GC策略(normal/size/reference-time)
-XX:ShenandoahGCHeuristics=normal
# 设置最大停顿时间目标(毫秒)
-XX:MaxGCPauseMillis=100
III、垃圾收集器对比表
收集器 | 新生代算法 | 老年代算法 | Full GC 收集器 | JDK版本支持 | 默认收集器(JDK版本) | 推荐堆大小 | 吞吐量 | 适用场景 | 是否开源 | 所属JVM实现 | 垃圾对象判定算法 | 核心特点 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Serial | 复制 | 标记-整理 | Serial Old(标记-整理) | JDK 1.0+ | JDK 1.0~8(客户端模式) | <512MB | 低 | 客户端应用、嵌入式系统 | 是 | HotSpot | 可达性分析(迭代标记) | 单线程简单高效,无多线程开销 |
Parallel | 复制 | 标记-整理 | Parallel Old(标记-整理) | JDK 1.4+ | JDK 8(服务器模式) | 512MB~32GB | 高 | 批处理、计算密集型任务 | 是 | HotSpot | 可达性分析(并行标记) | 多线程并行回收,最大化吞吐量 |
CMS | 复制(ParNew) | 标记-清除 | Serial Old(标记-整理) | JDK 1.5~JDK14 (JDK 9+ 标记为废弃,JDK 14+ 移除) | 无(需手动启用) | 512MB~4GB | 中 | 低延迟Web服务(中小堆) | 是 | HotSpot | 可达性分析(三色标记 + 增量更新) | 低延迟,并发标记减少停顿,但需监控碎片问题 |
G1 | 复制 | 标记-整理(分Region) | Serial Old(标记-整理) | JDK 7u4+ | JDK 9+(服务器模式) | 4GB~64GB | 中~高 | 大堆内存、平衡型应用 | 是 | HotSpot | 可达性分析(三色标记 + SATB快照) | 分区回收,可预测停顿,自动整理碎片 |
ZGC | 并发复制 | 并发标记-整理 | 无(无 Full GC 概念) | OpenJDK 11+ | JDK 15+(可选) | ≥4GB | 中 | 超大堆、亚毫秒级停顿 | 是 | HotSpot | 可达性分析(并发三色标记 + 染色指针) | 超大堆,染色指针,停顿<1ms |
Shenandoah | 并发复制 | 并发复制 | 无(无 Full GC 概念) | OpenJDK 12+ | 无(需手动启用) | ≥4GB | 中 | 超大堆、极低延迟需求 | 是 | HotSpot | 可达性分析(并发三色标记 + 读屏障) | 全堆并发,停顿<10ms |
IIII、垃圾对象判定算法详解
收集器 | 判定算法实现 |
---|---|
Serial | 递归遍历GC Roots,标记可达对象(无并发优化)。 |
Parallel | 多线程并行遍历GC Roots,标记可达对象。 |
CMS | 三色标记法 + 增量更新(将修改引用的黑色对象重新标记为灰色)。 |
G1 | 三色标记法 + SATB(原始快照)(基于标记开始时的引用关系进行标记)。 |
ZGC | 并发三色标记法 + 染色指针(通过指针元数据记录对象状态)。 |
Shenandoah | 并发三色标记法 + 读屏障(拦截对象访问,处理并发标记中的引用变化)。 |
五、GC 日志分析与可视化工具
5.1 典型 GC 日志示例
[GC (Allocation Failure) [PSYoungGen: 524288K->1024K(524288K)] 524288K->1025K(1048576K), 0.0012345 secs]
解析:
- GC:新生代 GC。
- Full GC:整堆 GC。
- Allocation Failure:因内存分配失败触发 GC。
- PSYoungGen:Parallel Scavenge 收集器的新生代。
- 524288K->1024K:回收前→回收后的内存使用量。
- 0.0012345 secs:GC 耗时。
5.2 可视化工具推荐
- GCViewer:解析 GC 日志,生成可视化图表(如停顿时间、内存使用趋势)。
- GCEasy:在线工具,上传 GC 日志后提供详细分析报告和优化建议。
- JProfiler:集成 GC 分析功能,支持实时监控和堆转储分析。
- Java Mission Control (JMC):Oracle 官方工具,提供高性能监控和诊断。
- Arthas(阿尔萨斯):阿里巴巴开源的 实时诊断工具,通过命令行或 Web UI 动态监控和排查问题。
- VisualVM:轻量级可视化工具,集成多种性能分析功能(含 GC 监控),支持插件扩展。
六、性能调优实战案例
6.1 高并发 Web 应用调优
场景: 8 核 16GB 服务器,Tomcat 应用,响应时间敏感。
配置:
java -Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=8m \
-XX:+ParallelRefProcEnabled \
-jar app.jar
效果:
- 新生代 GC 频率降低,停顿时间控制在 50ms 以内。
- 老年代碎片化问题缓解,Full GC 频率从每天数次降为每周一次。
6.2 大数据批处理调优
场景: Spark 作业,64GB 内存,追求高吞吐量。
配置:
spark-submit --conf spark.executor.memory=48g \
--conf spark.executor.extraJavaOptions="-XX:+UseParallelGC -XX:GCTimeRatio=99" \
--class com.example.BatchJob app.jar
效果:
- 吞吐量提升 20%,GC 时间占比从 5% 降至 1%。
- 并行 GC 充分利用多核 CPU,减少 STW 时间。
七、总结与最佳实践
垃圾判定:优先使用可达性分析,避免引用计数法的循环引用问题。
算法选择:
- 新生代:对象存活率低→复制算法。
- 老年代:对象存活率高→标记 - 整理算法。
收集器选型:
- 小内存 / 单线程:Serial。
- 吞吐量优先:Parallel。
- 低延迟:CMS(老年代)、G1(大内存)、ZGC/Shenandoah(超大内存)。
性能监控:
- 定期分析 GC 日志,监控停顿时间和内存使用趋势。
- 使用可视化工具定位内存泄漏和大对象问题。
通过结合图解和实战案例,可更直观地理解 JVM 垃圾回收机制的底层原理,从而针对性地优化应用性能。
附:
- JVM图解
- ⚙️运行时数据区的初始化时机
运行时数据区 | 初始化时机 | 与类加载的关系 |
---|---|---|
方法区 | JVM 启动时分配内存,类加载时写入类元信息、常量池、静态变量等。 | 类加载的初始化阶段会向方法区写入静态变量。 |
堆 | JVM 启动时分配内存,对象实例在 new 时创建。 | 类初始化完成后,通过 new 触发对象分配。 |
虚拟机栈 | 线程创建时初始化,每个线程有独立栈空间,栈帧在方法调用时动态生成。 | 与类加载无关,由线程执行引擎管理。 |
程序计数器 | 线程创建时初始化,记录当前执行指令地址。 | 与类加载无关,由线程执行引擎管理。 |
本地方法栈 | 线程创建时初始化,存储本地方法(Native Method)调用信息。 | 与类加载无关,由本地方法接口(JNI)管理。 |
- 类加载与运行时数据区的关系