JVM 的对象存活判定算法

本文最后更新于 2025年8月10日 21:11

引用计数法

给对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1
  • 当引用失效,计数器就减 1
  • 任何时候,计数器为 0 的对象不可能再被使用

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是:很难解决对象之间循环引用的问题。因为互相引用对方,导致二者的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

可达性分析算法

这个算法的基本思想就是通过一系列的称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可达的,需要被回收。目前常用的可达性分析算法有:三色标记、增量更新、快照引入、分代可达性分析

哪些对象可以作为 GC Roots

可作为 GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)执行上下文(例如栈帧中的本地变量表)中。

  • 虚拟机栈栈帧中引用的对象
  • 本地方法栈栈帧中引用的对象
  • 被类加载器加载并且活跃的类的静态变量
  • 字符串常量和在方法区中的其他常量
  • 所有被同步锁持有的对象

对象可以被回收,就代表一定会被回收吗

当一个对象是不可达的,JVM 会先标记这个对象为准备被回收。然后它会检查这个对象有没有覆盖 finalize() 方法,也就是 Java 中一个类似“遗言”的方法。

  • 如果这个对象没有写过 finalize(),或者这个方法以前已经执行过了,那就没必要再给它机会,这对象真的死定了,下一步就会被回收。
  • 如果这个对象有 finalize() 方法,而且还从来没被调用过,那 JVM 会把它放到一个队列里,准备执行 finalize() 方法,让它再挣扎一下。

finalize() 方法执行之后,JVM 会再一次做判断:

  • 如果在 finalize() 方法中,这个对象成功让自己重新和某些活着的对象建立了引用关系(比如把自己赋值给了某个静态变量),那它就“复活”了,GC 暂时不会回收它。
  • 如果还是没有任何人引用它,那这一次它就真的死了,GC 会把它清除掉。

三色标记算法

三色标记算法指的是将所有对象分为白色、黑色和灰色三种类型。

  • 白色:表示对象是未被标记的,即这个对象是垃圾回收的候选对象。
  • 灰色:表示对象已经被标记,但它的引用对象尚未被遍历。
  • 黑色:表示对象已经被标记,且它的引用对象也已经被遍历。

b5f1c26a5367faadf161b8c7d4632375.png

三色标记算法的执行流程

  • 初始阶段
    • 在垃圾回收的开始,所有的对象都是 白色
    • GC Roots 对象被标记为 灰色,即它们是活动的对象,并会开始遍历其引用的对象。
  • 标记阶段
    • 灰色对象 会扫描它所引用的对象。如果引用对象是 白色,则将其标记为 灰色;如果是 灰色黑色,则不做处理。
    • 被标记为 灰色 的对象会继续递归地扫描它们所引用的对象。
  • 黑色阶段
    • 一旦一个 灰色对象 的所有引用对象都被处理完,它会被标记为 黑色
    • 此时,表示该对象的引用已经完全被扫描完毕,且它的子对象都已被标记。
  • 清除阶段
    • 最后,所有的 白色对象 即不再被引用的对象会被垃圾回收清除。

三色标记的多标和漏标问题

多标问题

多标问题指的是原本应该回收的对象,被多余地标记为黑色存活对象,从而导致该垃圾对象没有被回收。

举个例子,a 引用了 b,此时 b 被扫描为可达,但是用户线程随后又执行了 a.b = null,这个时候其实 b 已经是死亡的垃圾对象了,但是由于黑色对象不会被重新扫描,所以在垃圾收集里 b 依然作为存活对象被标记成黑色,因此就产生了多标问题。但其实多标问题是可以容忍的,因为逃过本次收集的死亡对象,在下一次垃圾收集中就会被清理掉。

image.png

漏标问题

漏标问题指的是原本应该被标记为存活的对象,被遗漏标记为黑色,从而导致该垃圾对象被错误回收。下图演示了这样的致命错误具体是如何产生的:

image.png

如图所示,b -> c 的引用被切断,但同时用户线程建立了一个新的从 a -> c 的引用,由于已经遍历到了 b,不可能再回去遍历 a 再遍历 c,所以就导致实际存活的对象 c 被漏标了。

漏标问题的解决方案

正如前面分析,三色标记算法会造成漏标和多标问题。但多标问题并没有那么严重,漏标问题才是最严重的。我们经过分析可以知道,漏标问题要发生需要满足如下两个充要条件:

  1. 至少一个黑色对象在自己被标记之后指向了白色对象
  2. 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用

只有当上面两个条件都满足,三色标记算法才会发生漏标的问题。换言之,如果我们破坏任何一个条件,这个白色对象就不会被漏标。这其实就产生了两种解决漏标的方式,分别是:增量更新、原始快照。CMS 回收器使用的增量更新方案,G1 回收器采用的是原始快照方案。

  • 增量更新(Incremental Update):当黑色对象插入新的指向白色对象的引用关系时(就是上图中的 a -> c 引用关系),就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象(a)为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了
  • 原始快照(Snapshot At The Beginning,SATB):在 SATB 快照算法中,GC 在开始标记时认为对象图是静止的。为了保持这种快照语义,如果在并发标记过程中,某个线程删除了对某对象的引用(即把某个对象变得“不可达”),则需要将这个被删除引用的对象记录下来,并在标记阶段中继续将它视为可达,从而避免误回收仍存活的对象。

引用类型

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK 1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK 1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。强引用就是 Java 中普通的对象,而软引用、弱引用、虚引用在 JDK 中定义的类分别是 SoftReferenceWeakReferencePhantomReference

强引用(Strong Reference)

  • 定义:普通的对象引用,比如 Object obj = new Object();

  • 回收情况:如果一个对象具有强引用,那么垃圾回收器在回收时不会回收该对象。只有当对象的强引用被解除(即变量变为 null),且没有其他引用指向它时,它才会被垃圾回收器回收。

  • 示例

    1
    2
    Object obj = new Object();  // 强引用
    obj = null; // 解除强引用

软引用(Soft Reference)

  • 定义:软引用通常用来描述一些可以在内存足够时存在,但在内存不足时可以被回收的对象。可以通过 SoftReference 类来创建。

  • 回收情况:当 JVM 内存空间不足时,垃圾回收器会回收这些对象,释放内存。否则,它们会一直存在。

  • 示例

    1
    2
    SoftReference<Object> softRef = new SoftReference<>(new Object());
    Object obj = softRef.get(); // 可以访问对象

弱引用(Weak Reference)

  • 定义:弱引用是比软引用更“弱”的引用。它用 WeakReference 类表示。

  • 回收情况:只要垃圾回收器运行,不管内存是否足够,任何一个被弱引用关联的对象都会被回收。

  • 示例

    1
    2
    WeakReference<Object> weakRef = new WeakReference<>(new Object());
    Object obj = weakRef.get(); // 对象被弱引用

虚引用(Phantom Reference)

  • 定义:虚引用是最弱的引用类型。通过 PhantomReference 类创建。

  • 回收情况:虚引用不会影响对象的生命周期,它仅用于跟踪对象被垃圾回收的状态。垃圾回收器会在回收对象之前将虚引用加入到引用队列中。

  • 示例

    1
    PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());

JVM 的对象存活判定算法
http://example.com/2025/04/08/JVM 的对象存活判定算法/
作者
Moonike
发布于
2025年4月8日
更新于
2025年8月10日
许可协议