JVM 垃圾回收机制全面解析:从原理到调优

JVM 垃圾回收机制全面解析:从原理到调优

引言

Java 虚拟机(JVM)的垃圾回收(Garbage Collection,简称 GC)机制是 Java 语言区别于 C/C++ 等需要手动管理内存的语言的核心特性之一。理解 JVM GC 机制,不仅能帮助我们写出更高质量的 Java 代码,还能在生产环境中快速定位和解决内存泄漏、性能瓶颈等问题。本文将深入剖析 JVM 垃圾回收的工作原理、常见垃圾回收器以及实战调优策略。

一、为什么需要垃圾回收

在 Java 程序运行过程中,对象会不断被创建。当一个对象不再被任何引用持有时,它就变成了「垃圾」,如果不清理这些垃圾,内存终将耗尽。GC 的核心职责就是自动识别并回收这些无用对象占用的内存空间,让开发者从繁琐的内存管理中解放出来。

1.1 引用计数法与可达性分析

判断对象是否存活主要有两种算法:

引用计数法:每个对象有一个引用计数器,当有引用指向它时计数器 +1,引用失效时 -1。计数器为 0 时说明对象已无用。这种算法简单高效,但无法解决循环引用问题。Python 采用了引用计数 + 标记-清除的混合方案。

可达性分析算法:从一系列「GC Roots」对象出发,沿着引用链向下搜索,所有不可达的对象即判定为可回收。GC Roots 包括:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等。HotSpot JVM 采用的就是这种算法。

二、分代收集理论

现代 JVM 的垃圾回收几乎都基于「分代假说」进行设计:将堆内存划分为不同区域,对不同生命周期的对象采用不同的回收策略,以最大化回收效率。

2.1 堆内存分区

经典的分代将堆划分为:

新生代(Young Generation):新创建的对象首先分配在这里。新生代又分为 Eden 区和两个 Survivor 区(通常称为 From 和 To/Survivor 2)。大多数对象在这里朝生夕灭,存活率较低,适合使用复制算法进行回收。

老年代(Old/Tenured Generation):经历过多次 GC 后仍然存活的对象会晋升到这里。老年代对象存活时间长,不适合使用复制算法,通常采用标记-整理或标记-清除算法。

方法区(Metaspace):存放类的元信息、常量池等。在 Java 8 之前用永久代实现,之后改为元空间,使用本地内存。

2.2 对象晋升规则

对象在新生代的「 Eden 区→ Survivor 区」之间来回复制,每次 GC 后年龄计数器 +1。当对象年龄达到阈值(默认 15,可通过 -XX:MaxTenuringThreshold 设置),就会晋升到老年代。大对象(超过阈值大小)会直接进入老年代。

三、垃圾回收算法详解
3.1 标记-清除算法(Mark-Sweep)

分为标记和清除两个阶段:首先标记出所有需要回收的对象,然后统一回收被标记的对象。这是最基础的算法,但存在两个明显问题:效率不稳定(标记和清除的效率会随着对象增多而下降),以及内存碎片化(回收后产生大量不连续内存空间)。

3.2 复制算法(Copying)

将可用内存划分为大小相等的两块,每次只使用其中一块。当这一块内存用完时,就将还存活的对象复制到另一块上,然后一次性清除这块内存。优点是实现简单、运行高效,没有内存碎片问题;缺点是可用内存缩小为原来的一半,浪费空间。

新生代的 Minor GC 采用的就是改进的复制算法——将 Eden 区和其中一块 Survivor 区作为「From」空间,GC 时将存活对象复制到另一块「To」Survivor 区,然后交换角色。

3.3 标记-整理算法(Mark-Compact)

针对老年代的特点,标记-整理算法在标记后,不是直接清理可回收对象,而是让所有存活对象向一端移动,然后直接清理掉边界以外的内存。这样既避免了内存碎片,又充分利用了内存空间。

四、常见垃圾回收器

JVM 提供了多种垃圾回收器,它们可以组合使用。JDK 8 的默认组合是 Parallel Scavenge(新生代)+ Parallel Old(老年代)。

4.1 Serial 收集器

最基础、历史最悠久的收集器。采用单线程进行垃圾回收,在回收时必须暂停所有用户线程(Stop The World,STW)。简单高效,对于单核或小内存环境是很好的选择。通过 -XX:+UseSerialGC 启用。

4.2 ParNew 收集器

Serial 的多线程版本,使用多个线程并行进行垃圾回收。在多核 CPU 环境下,效率通常比 Serial 高。是 CMS 收集器(后文介绍)的老年代搭档。通过 -XX:+UseParNewGC 启用。

4.3 Parallel Scavenge 收集器

专注于提高吞吐量的收集器,通过参数可以控制最大 GC 停顿时间和吞吐量大小。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC 时间)。通过 -XX:+UseParallelGC 启用。

4.4 CMS 收集器

Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器。整个过程分为:初始标记(STW,标记 GC Roots 直接关联的对象)、并发标记(与用户线程并发,沿着引用链追踪)、重新标记(STW,修正并发标记期间产生的变化)和并发清除(与用户线程并发,清除垃圾)。

CMS 优点是并发收集、低停顿,缺点是对 CPU 资源敏感(会占用 CPU 资源),无法处理浮动垃圾(并发清理阶段产生的垃圾),以及由于使用标记-清除算法导致内存碎片。通过 -XX:+UseConcMarkSweepGC 启用。

4.5 G1 收集器

Garbage First,是 JDK 9+ 的默认垃圾回收器。G1 将堆划分为大小相等的 Region,每个 Region 可以独立作为 Eden、Survivor 或老年代。G1 的设计目标是实现可预测的停顿时间模型,通过 -XX:MaxGCPauseMillis 参数指定目标停顿时间。

G1 的工作流程包括:初始标记(停顿,标记直接可达的 GC Roots)、并发标记(并发追踪引用链)、最终标记(停顿,处理遗留的 SATB 记录)和筛选回收(停顿,对各个 Region 的回收价值和成本排序,根据用户期望的停顿时间制定回收计划)。

4.6 ZGC 与 Shenandoah

这是两款面向低延迟的收集器,致力于将 STW 时间控制在毫秒级甚至更低。

ZGC(Z Garbage Collector)在 JDK 11 实验性引入,JDK 15 生产可用。ZGC 通过着色指针和读屏障技术,实现了并发标记、并发压缩,整个 GC 过程中只有极短的 STW(通常不超过 1ms)。通过 -XX:+UseZGC 启用。

Shenandoah 由 Red Hat 开发,在 JDK 12 实验性引入。设计目标与 ZGC 类似,但实现方式有所不同。

五、G1 垃圾回收器深入解析

作为当前主流的垃圾回收器,G1 值得我们深入了解。

5.1 Region 设计

G1 将堆划分为约 2048 个大小相等的 Region,每个 Region 大小为 1MB~32MB(必须是 2 的幂)。逻辑上连续,但物理上分散。每个 Region 可以独立作为 Eden、Survivor 或 Humongous(大对象区,存储超过 Region 大小 50% 的对象)。

5.2 Young GC

当 Eden 区满了,G1 触发 Young GC。多线程并行将 Eden 区和 Survivor 区的存活对象复制到新的 Survivor 区和老年代。完成后交换 Eden 区和 Survivor 区。

5.3 Mixed GC

当老年代占用超过阈值(Initiating Heap Occupancy Percent,默认 45%),G1 触发 Mixed GC,收集所有年轻代和部分老年代 Region。Mixed GC 是 G1 实现可预测停顿的核心机制。

六、实战调优策略
6.1 常见 GC 参数

参数 说明
-Xms / -Xmx 堆最小/最大大小
-Xmn 新生代大小
-XX:+UseG1GC 使用 G1 收集器
-XX:MaxGCPauseMillis=200 设置 GC 目标停顿时间
-XX:G1HeapRegionSize=16 设置 Region 大小
-XX:InitiatingHeapOccupancyPercent=45 触发 Mixed GC 的老年代阈值

6.2 调优思路

基本原则:先了解应用的内存分配特点(是对象生命周期短的新生代密集型,还是对象长期存活的老年代密集型),再选择合适的收集器和参数。

案例一:GC 频繁但时间短
如果 Minor GC 频繁但每次停顿时间可接受,可以考虑增大新生代大小(-Xmn)或调整 -XX:MaxGCPauseMillis,让 G1 更从容地处理垃圾。

案例二:Full GC 频繁
检查是否有内存泄漏(大量对象进入老年代后无法回收)。可以使用 jmap -heapjstat -gcutil 等工具分析堆使用情况,考虑使用 G1 的 -XX:InitiatingHeapOccupancyPercent 调整 Mixed GC 触发时机。

6.3 常用诊断命令

# 查看堆内存使用情况
jmap -heap <pid>

# 查看 GC 统计信息
jstat -gcutil <pid> 1000

# 生成堆转储快照
jmap -dump:format=b,file=heap.hprof <pid>

# 查看 GC 日志
jinfo -flags <pid> | grep GC

七、总结

JVM 垃圾回收机制是 Java 高性能的核心保障。从标记算法到分代收集,从 Serial 到 ZGC,每一种设计都针对不同的应用场景进行了优化。理解这些原理,能够帮助我们在面对 GC 调优问题时不再迷茫,而是有章可循地去分析问题、解决问题。

在实际工作中,建议优先使用 G1 收集器,它是目前最通用且功能均衡的选择。对于对延迟极其敏感的场景,可以考虑 ZGC。同时,善用 JFR(JDK Flight Recorder)和各种诊断工具,让数据说话,是 GC 调优的不二法门。

参考资料

  • 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》周志华
  • Oracle 官方 JVM 文档
  • OpenJDK 源码

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注