6.2 垃圾回收 (Garbage Collection - GC) 6.2 垃圾回收 (Garbage Collection - GC) 垃圾回收 (Garbage Collection, GC) 是 JVM 中至关重要的自动内存管理机制。它负责识别并回收不再使用的对象,释放其占用的内存,从而避免内存泄漏,保证应用程序的稳定运行。开发者无需显式地释放内存,GC 会在后台自动执行这项任务,极大地简化了内存管理,降低了开发难度。 6.2.1 垃圾回收的必要性 在没有垃圾回收机制的编程语言中,程序员需要手动分配和释放内存。这容易导致以下问题: 内存泄漏 (Memory Leak): 分配的内存没有被释放,导致可用内存逐渐减少,最终耗尽系统资源。
垃圾回收 (Garbage Collection, GC) 是 JVM 中至关重要的自动内存管理机制。它负责识别并回收不再使用的对象,释放其占用的内存,从而避免内存泄漏,保证应用程序的稳定运行。开发者无需显式地释放内存,GC 会在后台自动执行这项任务,极大地简化了内存管理,降低了开发难度。
在没有垃圾回收机制的编程语言中,程序员需要手动分配和释放内存。这容易导致以下问题:
内存泄漏 (Memory Leak): 分配的内存没有被释放,导致可用内存逐渐减少,最终耗尽系统资源。
悬挂指针 (Dangling Pointer): 释放了内存,但仍有指针指向这块内存,导致程序崩溃或数据损坏。
重复释放 (Double Free): 同一块内存被多次释放,导致内存管理混乱。
垃圾回收机制通过自动管理内存,有效地避免了这些问题,提高了程序的可靠性和开发效率。
垃圾回收的核心任务是识别哪些对象是“垃圾”,即不再被程序使用的对象。通常,GC 采用以下两种基本策略来识别垃圾:
引用计数 (Reference Counting): 为每个对象维护一个引用计数器,记录指向该对象的引用数量。当引用计数为 0 时,表示该对象不再被引用,可以被回收。
优点: 实现简单,回收及时。
缺点: 无法解决循环引用问题,例如两个对象互相引用,即使它们不再被程序使用,引用计数仍然不为 0。
可达性分析 (Reachability Analysis): 从一组被称为 "GC Roots" 的根对象开始,沿着引用链向下搜索,所有能够被 GC Roots 访问到的对象都被认为是“可达的”,不能被回收。反之,无法被 GC Roots 访问到的对象则被认为是“垃圾”,可以被回收。
优点: 可以解决循环引用问题。
缺点: 需要进行全局扫描,开销较大。
JVM 采用可达性分析作为主要的垃圾回收算法。
GC Roots 是指一组活跃的引用,它们是垃圾回收的起点。常见的 GC Roots 包括:
栈帧中的局部变量: 当前正在执行的方法的局部变量。
静态变量: 类级别的静态变量。
常量池中的引用: 常量池中指向对象的引用。
JNI (Java Native Interface) 引用: 本地方法持有的对象引用。
在上图中,Object 1, Object 2, Object 3, Object 4 都是可达对象,Object 5 和 Object 6 是不可达对象,会被GC回收。
JVM 提供了多种垃圾回收算法,每种算法都有其优缺点,适用于不同的场景。常见的垃圾回收算法包括:
标记-清除 (Mark and Sweep):
标记 (Mark): 从 GC Roots 开始,标记所有可达对象。
清除 (Sweep): 遍历整个堆,回收未被标记的对象。
优点: 实现简单。
缺点: 会产生大量的内存碎片,导致后续分配大对象时可能找不到连续的内存空间。
复制 (Copying): 将堆分成两个区域,每次只使用其中一个区域。当一个区域满了之后,将所有存活的对象复制到另一个区域,然后清理整个区域。
优点: 避免了内存碎片问题,分配内存简单高效。
缺点: 浪费空间,每次只能使用一半的堆空间。
标记-整理 (Mark and Compact):
标记 (Mark): 从 GC Roots 开始,标记所有可达对象。
整理 (Compact): 将所有存活的对象移动到堆的一端,然后清理边界外的所有内存。
优点: 避免了内存碎片问题,空间利用率高。
缺点: 需要移动对象,开销较大。
分代收集 (Generational Collection): 根据对象的生命周期将堆分成不同的区域,针对不同区域采用不同的垃圾回收算法。
新生代 (Young Generation): 存放新创建的对象,对象存活时间短,采用复制算法。
老年代 (Old Generation): 存放存活时间长的对象,采用标记-清除或标记-整理算法。
永久代/元空间 (Permanent Generation/Metaspace): 存放类信息、常量、静态变量等,通常使用标记-清除或标记-整理算法。
分代收集是 JVM 中最常用的垃圾回收策略。
垃圾回收器是垃圾回收算法的具体实现。JVM 提供了多种垃圾回收器,开发者可以根据应用程序的特点选择合适的垃圾回收器。常见的垃圾回收器包括:
Serial Collector: 单线程垃圾回收器,在垃圾回收时会暂停所有用户线程 (Stop-The-World, STW)。适用于单核 CPU 或小内存的场景。
Parallel Collector: 多线程垃圾回收器,可以并行执行垃圾回收,减少 STW 时间。适用于多核 CPU 的场景。
Concurrent Mark Sweep (CMS) Collector: 并发垃圾回收器,允许垃圾回收线程和用户线程并发执行,减少 STW 时间。但 CMS 会产生内存碎片,并且在回收过程中可能会出现 "Concurrent Mode Failure",导致 Full GC。
Garbage First (G1) Collector: 面向局部垃圾回收的垃圾回收器,将堆分成多个区域 (Region),优先回收垃圾最多的区域。G1 可以更好地控制 STW 时间,适用于大内存的场景。
Z Garbage Collector (ZGC): 低延迟垃圾回收器,适用于对延迟要求非常高的场景。ZGC 采用染色指针技术,可以实现并发的标记、整理和迁移。
Shenandoah: 与ZGC类似,也是一种并发的低延迟垃圾回收器。
垃圾回收通常在以下情况下触发:
堆空间不足: 当堆空间的使用率达到一定的阈值时,会触发垃圾回收。
系统空闲: 当系统空闲时,可以触发垃圾回收,释放内存资源。
手动触发: 可以通过 System.gc() 方法手动触发垃圾回收,但这只是建议 JVM 执行垃圾回收,并不保证立即执行。
以下代码示例演示了如何创建一个对象,并在不再使用时将其设置为 null,以便垃圾回收器回收。
public class GarbageCollectionExample { public static void main(String[] args) { // 创建一个对象 MyObject obj = new MyObject("Example Object"); // 使用对象 System.out.println(obj.getName()); // 将对象设置为 null,使其成为垃圾 obj = null; // 建议 JVM 执行垃圾回收 System.gc(); // 等待一段时间,以便垃圾回收器执行 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Garbage collection completed (possibly)."); } } class MyObject { private String name; public MyObject(String name) { this.name = name; } public String getName() { return name; } @Override protected void finalize() throws Throwable { System.out.println("Object " + name + " is being garbage collected."); } }
代码解释:
MyObject 类定义了一个简单的对象,包含一个 name 属性和一个 finalize() 方法。finalize() 方法在对象被垃圾回收之前调用,用于执行一些清理工作。
在 main 方法中,我们创建了一个 MyObject 对象,并将其赋值给变量 obj。
在使用完对象后,我们将 obj 设置为 null,这意味着该对象不再被引用,可以被垃圾回收。
System.gc() 方法建议 JVM 执行垃圾回收。
Thread.sleep(1000) 方法等待一段时间,以便垃圾回收器执行。
finalize() 方法会在对象被垃圾回收之前被调用,打印一条消息。
注意事项:
System.gc() 方法只是建议 JVM 执行垃圾回收,并不保证立即执行。
finalize() 方法不应该被用于执行重要的清理工作,因为它执行的时机是不确定的,并且可能会影响垃圾回收的性能。建议使用 try-with-resources 语句或显式地调用 close() 方法来释放资源。
避免创建不必要的对象,减少垃圾回收的压力。
垃圾回收的调优是一个复杂的过程,需要根据应用程序的特点进行调整。常见的垃圾回收调优策略包括:
选择合适的垃圾回收器: 根据应用程序的特点选择合适的垃圾回收器,例如,对于延迟要求高的应用程序,可以选择 ZGC 或 Shenandoah。
调整堆大小: 根据应用程序的内存需求调整堆大小,避免频繁的垃圾回收。
调整新生代和老年代的比例: 根据应用程序的对象生命周期调整新生代和老年代的比例,优化垃圾回收的效率。
使用垃圾回收监控工具: 使用垃圾回收监控工具,例如 JConsole、VisualVM 等,监控垃圾回收的性能,并根据监控结果进行调整。
避免创建不必要的对象: 减少垃圾回收的压力,提高应用程序的性能。
垃圾回收是 JVM 中至关重要的自动内存管理机制。理解垃圾回收的基本原理、垃圾回收算法和垃圾回收器,可以帮助开发者更好地编写高效、稳定的 Java 应用程序。通过合理的垃圾回收调优,可以提高应用程序的性能,减少延迟,提升用户体验。