6.2 垃圾回收 (Garbage Collection - GC)


文档摘要

6.2 JVM 垃圾回收 (Garbage Collection) 机制详解 核心摘要:JVM 垃圾回收(Garbage Collection, 简称 GC)是 Java 虚拟机中至关重要的自动内存管理机制。本文深度解析 JVM 垃圾回收的基本原理、GC Roots 可达性分析、主流垃圾回收算法(标记-清除、复制、标记-整理)以及各类垃圾回收器(G1、ZGC、Shenandoah 等)的应用场景。掌握 Java GC 机制与调优策略,能够有效避免内存泄漏,降低系统延迟,是构建高性能、高可用 Java 应用程序的核心基础。 6.2.1 为什么需要垃圾回收?JVM 内存管理的必要性 在没有自动垃圾回收机制的底层编程语言(如 C/C++)中,开发者需要手动分配和释放内存。

6.2 JVM 垃圾回收 (Garbage Collection) 机制详解

核心摘要:JVM 垃圾回收(Garbage Collection, 简称 GC)是 Java 虚拟机中至关重要的自动内存管理机制。本文深度解析 JVM 垃圾回收的基本原理、GC Roots 可达性分析、主流垃圾回收算法(标记-清除、复制、标记-整理)以及各类垃圾回收器(G1、ZGC、Shenandoah 等)的应用场景。掌握 Java GC 机制与调优策略,能够有效避免内存泄漏,降低系统延迟,是构建高性能、高可用 Java 应用程序的核心基础。

6.2.1 为什么需要垃圾回收?JVM 内存管理的必要性

在没有自动垃圾回收机制的底层编程语言(如 C/C++)中,开发者需要手动分配和释放内存。这种手动管理方式极易引发以下严重的内存问题:

  • 内存泄漏 (Memory Leak):已分配的内存未被及时释放,导致可用内存逐渐减少,最终耗尽系统资源,引发 OutOfMemoryError
  • 悬挂指针 (Dangling Pointer):内存已被释放,但仍有指针指向该内存地址。再次访问时会导致程序崩溃或读取到脏数据。
  • 重复释放 (Double Free):同一块内存被多次释放,导致内存管理链表混乱,引发不可预知的系统崩溃。

JVM 通过引入垃圾回收机制,在后台自动识别并回收不再使用的对象,彻底解放了开发者的内存管理负担,大幅提高了程序的可靠性与开发效率。

6.2.2 JVM 垃圾回收的基本原理与 GC Roots

垃圾回收的核心任务是精准识别哪些对象是“垃圾”(即不再被程序使用的对象)。通常,GC 采用以下两种基本策略:

  1. 引用计数 (Reference Counting):为每个对象维护一个引用计数器。当引用计数为 0 时,表示该对象可被回收。
    • 优点:实现简单,回收及时。
    • 缺点:无法解决循环引用问题(如两个对象互相引用,计数永远不为 0)。因此,主流 JVM 并未采用此方案。
  2. 可达性分析 (Reachability Analysis):从一组被称为 GC Roots 的根对象开始,沿着引用链向下搜索。所有能被 GC Roots 访问到的对象被视为“可达”(存活),反之则被视为“不可达”(垃圾)。
    • 优点:完美解决循环引用问题。
    • 缺点:需要进行全局扫描,停顿时间(STW)较长。

JVM 采用可达性分析作为核心的垃圾回收算法。

什么是 GC Roots?

GC Roots 是一组活跃的引用,作为垃圾回收的起点。常见的 GC Roots 包括:

  • 虚拟机栈中引用的对象:当前正在执行的方法的局部变量、操作数栈中的引用。
  • 方法区中类静态属性引用的对象:类级别的静态变量(Static Variables)。
  • 方法区中常量引用的对象:常量池(Constant Pool)中指向堆内对象的引用。
  • 本地方法栈中 JNI 引用的对象:Java Native Interface (JNI) 持有的本地方法引用。

图示解析:在上图中,Object 1、2、3、4 均能通过引用链追溯到 GC Roots,属于可达对象;而 Object 5 和 Object 6 没有任何引用链相连,属于不可达对象,将在下一次 GC 时被回收。

6.2.3 主流垃圾回收算法解析

JVM 提供了多种垃圾回收算法,每种算法针对不同的内存场景进行了优化:

  • 标记-清除 (Mark and Sweep)
    • 过程:首先从 GC Roots 开始标记所有可达对象;然后遍历整个堆,回收未被标记的对象。
    • 缺点:会产生大量的内存碎片,导致后续分配大对象时可能找不到连续的内存空间,从而提前触发 Full GC。
  • 复制 (Copying)
    • 过程:将堆内存分为两个区域,每次只使用其中一个。当该区域满时,将所有存活对象复制到另一个区域,然后清空原区域。
    • 优点:避免了内存碎片,内存分配只需移动指针,极其高效。
    • 缺点:空间利用率低(默认浪费一半空间)。现代 JVM 通过调整比例(如 Eden 区与 Survivor 区的 8:1:1 划分)来优化此问题。
  • 标记-整理 (Mark and Compact)
    • 过程:标记过程与“标记-清除”一致,但后续步骤不是直接清理,而是将所有存活对象向内存一端移动,然后清理边界外的内存。
    • 优点:无内存碎片,空间利用率高。
    • 缺点:移动对象需要更新引用地址,停顿开销较大。
  • 分代收集 (Generational Collection)
    • 核心思想:根据对象的生命周期(弱代假说)将堆划分为不同区域,因地制宜采用不同算法。这是现代 JVM 最常用的策略。
    • 新生代 (Young Generation):存放新创建的对象,朝生夕死,存活率低。采用复制算法
    • 老年代 (Old Generation):存放经过多次 Minor GC 依然存活的对象。采用标记-清除标记-整理算法
    • 元空间 (Metaspace):JDK 8 之后取代了永久代,存放类元数据、常量等,直接使用本地内存(Native Memory),通常随老年代一起回收。

6.2.4 常见 JVM 垃圾回收器对比与选择

垃圾回收器是上述算法的具体实现。随着 JDK 版本的演进,JVM 提供了多种高性能回收器:

  • Serial / Serial Old:单线程回收器,执行 GC 时会暂停所有用户线程 (Stop-The-World, STW)。适用于客户端模式或单核小内存场景。
  • Parallel Scavenge / Parallel Old:多线程回收器,关注吞吐量(CPU 用于运行用户代码的时间占比)。适用于后台计算密集型任务。
  • CMS (Concurrent Mark Sweep):以获取最短回收停顿时间为目标的并发回收器。缺点是会产生内存碎片,且在并发阶段占用 CPU 资源,JDK 14 中已被彻底移除。
  • G1 (Garbage First):面向服务端应用的回收器。将堆划分为多个大小相等的 Region,优先回收价值最大(垃圾最多)的区域。能够建立可预测的停顿时间模型,是 JDK 9 之后的默认回收器。
  • ZGC (Z Garbage Collector):超低延迟回收器。采用染色指针和读屏障技术,将 STW 时间控制在 1 毫秒以内,且停顿时间不随堆大小增加而增加。JDK 21 引入了分代 ZGC,性能进一步提升。
  • Shenandoah:由 Red Hat 开发的低延迟回收器,与 ZGC 目标类似,采用 Brooks 指针实现并发整理。

6.2.5 垃圾回收的触发条件与时机

JVM 会根据内存使用情况自动触发不同级别的垃圾回收:

  1. Minor GC (Young GC):当新生代(Eden 区)空间不足,无法为新对象分配内存时触发。
  2. Major GC / Full GC
    • 老年代空间不足。
    • 元空间(Metaspace)空间不足。
    • 调用 System.gc() 方法(仅为建议,不保证执行)。
    • 空间分配担保失败(Survivor 区放不下,且老年代剩余空间小于历次晋升到老年代对象的平均大小)。
  3. 并发回收触发:如 G1 的 Mixed GC 或 CMS 的并发标记,通常由老年代占用率达到特定阈值(如 -XX:InitiatingHeapOccupancyPercent)时触发。

6.2.6 Java 垃圾回收代码实践与避坑指南

在早期 Java 版本中,开发者常通过重写 finalize() 方法来处理资源释放。注意:finalize() 方法已在 Java 9 中被标记为废弃(Deprecated),并在后续版本中受到严格限制,因其执行时机不确定且严重影响 GC 性能。

现代 Java 开发中,推荐使用 AutoCloseable 接口配合 try-with-resources 语句来管理资源:

public class GarbageCollectionExample { public static void main(String[] args) { // 使用 try-with-resources 自动管理资源,确保 close() 被调用 try (MyResource resource = new MyResource("Database Connection")) { System.out.println("Using resource: " + resource.getName()); } // 离开作用域时,自动调用 close() 方法释放底层资源 // 制造一些垃圾对象 for (int i = 0; i < 10000; i++) { new MyResource("Temp Object " + i); } // 建议 JVM 执行垃圾回收(仅用于演示,生产环境严禁滥用) System.gc(); System.out.println("Resource management and GC suggestion completed."); } } class MyResource implements AutoCloseable { private final String name; public MyResource(String name) { this.name = name; } public String getName() { return name; } @Override public void close() { // 在此处执行确定性的资源清理工作(如关闭文件流、数据库连接) System.out.println("Resource [" + name + "] is explicitly closed and cleaned up."); } }

核心实践原则:

  • 摒弃 finalize():对于必须执行的清理逻辑,使用 try-with-resourcesjava.lang.ref.Cleaner(Java 9+)。
  • 避免滥用 System.gc():这会强制触发 Full GC,导致系统长时间停顿,严重影响线上服务吞吐量。
  • 对象池化与复用:对于频繁创建和销毁的大对象(如线程池、数据库连接池),使用池化技术减轻 GC 压力。

6.2.7 JVM 垃圾回收调优策略与最佳实践

GC 调优的核心目标是:降低停顿时间(Latency)、提高吞吐量(Throughput)、控制内存占用(Footprint)。常见的调优策略包括:

  1. 选择合适的垃圾回收器
    • 常规 Web 应用(堆内存 < 8GB):首选 G1
    • 大内存且对延迟极度敏感的应用(堆内存 > 8GB):选择 ZGCShenandoah
  2. 合理设置堆内存大小
    • 将初始堆大小 (-Xms) 和最大堆大小 (-Xmx) 设置为相同值,避免堆内存动态扩容带来的性能开销。
    • 示例:-Xms4g -Xmx4g
  3. 优化分代比例
    • 如果应用产生大量短生命周期对象,可适当扩大新生代比例(-Xmn-XX:NewRatio)。
  4. 启用 GC 日志与监控
    • 开启详细 GC 日志以便事后分析:-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M (JDK 9+ 统一日志框架)。
    • 使用 Arthas、JProfiler 或 Prometheus + Grafana 实时监控 GC 频率与 STW 时间。
  5. 代码级优化
    • 避免在循环体内创建不必要的对象。
    • 慎用 String 拼接,改用 StringBuilder
    • 及时将不再使用的长生命周期对象引用置为 null(尤其是在长方法或静态集合中)。

6.2.8 总结

JVM 垃圾回收机制是 Java 语言实现“一次编写,到处运行”且保持高稳定性的基石。深入理解可达性分析原理、掌握各类垃圾回收算法的优劣,并能根据业务场景精准选择与调优 G1、ZGC 等现代垃圾回收器,是每一位高级 Java 工程师的必修课。通过合理的 JVM 参数配置与规范的代码实践,开发者能够最大化地释放硬件性能,打造出低延迟、高吞吐的企业级应用。


发布者: 作者: 转发
评论区 (0)
U