JVM 垃圾回收机制全景解析:从对象回收到收集算法

发布于:2025-07-29 ⋅ 阅读:(25) ⋅ 点赞:(0)

在 JVM 的运行时数据区中,堆内存是垃圾回收的核心战场 —— 这里存储着几乎所有的对象实例,而随着程序的运行,无用对象会不断堆积,若不及时清理,最终会导致内存溢出(OOM)。垃圾回收(Garbage Collection,GC)机制通过自动识别并回收无用对象的内存,解决了手动管理内存的繁琐与风险(如内存泄漏、野指针)。本文将系统剖析垃圾回收的基础流程:从如何判定对象 “已死”,到核心回收算法的实现原理,为理解垃圾收集器的工作机制奠定基础。

一、对象存活判定:哪些对象需要被回收?

垃圾回收的第一步是确定 “哪些对象已经不再被使用”。JVM 采用两种核心算法来判定对象的存活状态:引用计数法和可达性分析算法。

1.1 引用计数法:简单但有缺陷的早期方案

算法原理:给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加 1;当引用失效时,计数器值减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

代码示意

public class ReferenceCountDemo {
    public Object instance;

    public static void main(String[] args) {
        ReferenceCountDemo objA = new ReferenceCountDemo();
        ReferenceCountDemo objB = new ReferenceCountDemo();
        
        objA.instance = objB; // objB的引用计数+1
        objB.instance = objA; // objA的引用计数+1
        
        objA = null; // objA的引用计数-1(此时不为0)
        objB = null; // objB的引用计数-1(此时不为0)
        // 此时objA和objB互相引用,计数器均为1,无法被回收
    }
}

致命缺陷:无法解决循环引用问题(如示例中objA和objB互相引用,即使两者都已无用,计数器仍不为 0,导致内存泄漏)。因此,主流 JVM(如 HotSpot)均未采用这种算法,而是选择可达性分析算法。

1.2 可达性分析算法:JVM 的主流选择

算法原理:以 “GC Roots” 为起点,向下搜索所走过的路径称为 “引用链”(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到该对象不可达)时,则证明该对象是无用的。

GC Roots 的常见类型

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象(如方法参数、局部变量);
  • 方法区中类静态属性引用的对象(如static变量指向的对象);
  • 方法区中常量引用的对象(如final变量指向的对象);
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象;
  • 虚拟机内部的引用(如基本数据类型对应的 Class 对象、异常对象NullPointerException)。

示例解析

在下图中,对象 A、B、C 到 GC Roots 均有引用链相连(A 直接被 GC Roots 引用,B 被 A 引用,C 被 B 引用),因此是存活对象;而对象 D、E 虽然互相引用,但无法到达 GC Roots,因此被判定为可回收对象。

(示意图:可达性分析算法中的存活与可回收对象)

1.3 引用的四种类型:决定对象的回收时机

在 JDK 1.2 之后,Java 对 “引用” 的概念进行了扩充,将其分为强引用、软引用、弱引用和虚引用四种,不同类型的引用决定了对象被回收的优先级:

  1. 强引用(Strong Reference)
    • 最常见的引用类型(如Object obj = new Object());
    • 只要强引用存在,垃圾收集器就不会回收被引用的对象;
    • 即使内存不足,JVM 也会抛出 OOM 异常,而不会回收强引用对象。
  1. 软引用(Soft Reference)
    • 用于描述有用但非必需的对象(如缓存数据);
    • 当内存充足时,不会被回收;当内存不足时,会被回收;
    • 可通过SoftReference类实现(如SoftReference<Object> softRef = new SoftReference<>(new Object()))。
  1. 弱引用(Weak Reference)
    • 用于描述非必需对象,强度比软引用更弱;
    • 无论内存是否充足,只要发生垃圾回收,就会被回收;
    • 可通过WeakReference类实现(如WeakReference<Object> weakRef = new WeakReference<>(new Object()))。
  1. 虚引用(Phantom Reference)
    • 最弱的一种引用关系,无法通过虚引用获取对象实例;
    • 唯一作用是在对象被回收时收到一个系统通知;
    • 必须配合引用队列(ReferenceQueue)使用(如PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue))。

应用场景

  • 强引用:普通对象的日常引用(如业务实体对象);
  • 软引用:内存敏感的缓存(如图片缓存,内存不足时自动清理);
  • 弱引用:生命周期短暂的缓存(如ThreadLocal中的键值对,避免内存泄漏);
  • 虚引用:跟踪对象回收状态(如管理直接内存的释放)。

1.4 对象的自我救赎:finalize () 方法的最后机会

即使对象被可达性分析判定为不可达,也并非 “必死无疑”—— 它还有一次自我救赎的机会,这个机会来自finalize()方法:

  1. 当对象被第一次标记为可回收时,会检查它是否重写了finalize()方法:
    • 若未重写,则直接判定为可回收;
    • 若重写了,则将对象放入 F-Queue 队列中,并由虚拟机自动创建的低优先级线程执行该方法(但不保证执行完成)。
  1. 在finalize()方法中,对象可通过重新与 GC Roots 建立引用链实现 “自我救赎”(如把自己赋值给某个强引用变量)。
  1. 稍后 JVM 会对 F-Queue 队列中的对象进行第二次标记,若对象已自救,则移除回收列表;否则,最终被判定为可回收对象。

代码示例

public class FinalizeDemo {
    public static FinalizeDemo instance;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("执行finalize()方法");
        instance = this; // 自我救赎:重新建立强引用
    }

    public static void main(String[] args) throws InterruptedException {
        instance = new FinalizeDemo();
        
        // 第一次让对象成为可回收对象
        instance = null;
        System.gc(); // 触发GC
        Thread.sleep(500); // 等待finalize()执行
        if (instance != null) {
            System.out.println("对象存活");
        } else {
            System.out.println("对象已回收");
        }
        
        // 第二次让对象成为可回收对象(finalize()只会执行一次)
        instance = null;
        System.gc();
        Thread.sleep(500);
        if (instance != null) {
            System.out.println("对象存活");
        } else {
            System.out.println("对象已回收");
        }
    }
}

输出结果


执行finalize()方法

对象存活

对象已回收

注意:finalize()方法运行代价高昂,且无法保证执行顺序,在实际开发中不推荐使用,应通过 try-finally 等机制管理资源。

二、垃圾回收算法:如何高效回收内存

当对象被判定为可回收后,垃圾收集器需要通过具体的算法来释放其占用的内存。主流的垃圾回收算法包括标记 - 清除算法、复制算法、标记 - 整理算法和分代收集算法。

2.1 标记 - 清除算法(Mark-Sweep):最基础的回收算法

算法流程

  1. 标记阶段:通过可达性分析,标记出所有需要回收的对象;
  1. 清除阶段:遍历堆内存,回收所有被标记的对象,释放其占用的内存。

示意图

(示意图:标记 - 清除算法的标记与清除过程)

缺点

  • 效率问题:标记和清除两个过程的效率都不高,随着对象数量增加,性能会下降;
  • 空间碎片问题:清除后会产生大量不连续的内存碎片,当需要分配大对象时,可能因找不到足够的连续内存而不得不提前触发另一次垃圾回收。

适用场景:由于存在明显缺陷,标记 - 清除算法一般不单独使用,但它是其他算法的基础。

2.2 复制算法(Copying):解决效率与碎片问题

算法流程

  1. 将堆内存划分为大小相等的两块(From 区和 To 区),每次只使用其中一块(From 区);
  1. 当 From 区内存用完时,触发 GC,标记出 From 区中的存活对象;
  1. 将存活对象复制到 To 区,并按顺序排列(消除碎片);
  1. 清空 From 区,交换 From 区和 To 区的角色(下次使用新的 From 区)。

示意图

(示意图:复制算法的复制与清理过程)

优点

  • 效率高:只需复制存活对象,且复制过程中自然消除内存碎片;
  • 实现简单:无需处理复杂的内存碎片问题。

缺点

  • 内存利用率低:只能使用一半的堆内存(如 100MB 堆,实际可用仅 50MB);
  • 当存活对象比例较高时(如老年代),复制成本会显著增加。

适用场景:适用于存活对象少的区域,如新生代(Young Generation)—— 研究表明,新生代中的对象 98% 都是 “朝生夕死” 的,因此复制算法在新生代非常高效。

2.3 标记 - 整理算法(Mark-Compact):老年代的首选算法

算法流程

  1. 标记阶段:与标记 - 清除算法相同,标记出所有需要回收的对象;
  1. 整理阶段:将所有存活对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

示意图

(示意图:标记 - 整理算法的标记与整理过程)

优点

  • 解决了标记 - 清除算法的空间碎片问题;
  • 内存利用率高(无需像复制算法那样预留一半空间)。

缺点

  • 增加了整理阶段的开销(移动存活对象并更新引用地址),效率低于复制算法。

适用场景:适用于存活对象多的区域,如老年代(Old Generation)—— 老年代对象存活率高,复制算法的成本过高,而标记 - 整理算法能在保证内存利用率的同时避免碎片。

2.4 分代收集算法(Generational Collection):组合拳策略

当前商业虚拟机的垃圾收集都采用 “分代收集” 算法,它并非新的算法,而是根据对象的存活周期将堆内存划分为不同区域(新生代和老年代),并针对不同区域采用最合适的回收算法:

  1. 新生代(Young Generation)
    • 特点:对象存活时间短,存活率低(大部分对象创建后很快被回收);
    • 回收算法:采用复制算法(高效处理短生命周期对象);
    • 结构细分:Eden 区(占比 80%)、Survivor From 区(10%)、Survivor To 区(10%)。
    • 回收流程:
      • 新对象优先分配到 Eden 区,当 Eden 区满时,触发 Minor GC(新生代 GC);
      • 将 Eden 区和 From 区的存活对象复制到 To 区,年龄计数器 + 1;
      • 交换 From 区和 To 区的角色,年龄达到阈值(默认 15)的对象晋升到老年代。
  1. 老年代(Old Generation)
    • 特点:对象存活时间长,存活率高(经过多次 Minor GC 仍存活);
    • 回收算法:采用标记 - 清除或标记 - 整理算法(减少内存碎片,适合长生命周期对象);
    • 回收触发:当老年代内存不足时,触发 Major GC(老年代 GC),通常会伴随 Minor GC,耗时较长。
  1. 永久代 / 元空间(Permanent Generation/Metaspace)
    • 存储类元数据、常量、静态变量等,JDK 1.8 后永久代被元空间(Metaspace)取代(元空间使用本地内存);
    • 当元空间内存不足时,会触发 Full GC(全堆 GC)。

分代收集的优势

通过将不同生命周期的对象分开管理,针对性地选择回收算法,兼顾了垃圾回收的效率和内存利用率,是目前最成熟的 GC 方案。


网站公告

今日签到

点亮在社区的每一天
去签到