JVM 垃圾回收机制:可达性分析算法详解
虽然方法区也有 GC,但条件更为苛刻(如所有实例被回收、ClassLoader 被回收等),本文主要讲解堆内存。
可达性分析算法
垃圾收集器通常采用可达性分析算法。其核心规则是:如果某个对象没有通过引用链连接到 GC Roots,则表明该对象不可达,会被判定为可回收对象。
GC Roots 是一个数据结构,包含多种引用类型。例如,在方法中 new 出来的对象会被强引用连接。
根据 JVM 内存区域的分配,new Student() 是在 Java 堆中开辟了一块空间用于存放对象数据。Java 中虽取消了显式指针,但变量本质上起到了指针的作用。如果将变量设置为 null,代表将引用指向的位置断开,此时 Java 堆中的对象就会被可达性算法检测为'GC Roots 到该对象不可达'。
finalize 方法
finalize 是所有类中都存在的一个方法,它只有在 GC 过程中会被触发使用,而且只会被使用一次,并且已被官方弃用。
书中提到:它并不能等同于 C 和 C++ 语言中的析构函数,而是 Java 刚诞生时为了使传统 C/C++ 程序员更容易接受 Java 所做出的一项妥协。它的运行代价高昂、不确定性大、无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。
在 Java 源码中也如是说到:子类重写 finalize 方法以处置系统资源或执行其他清理。finalize 的一般约定是:如果 Java 虚拟机确定任何尚未死亡的线程都无法再访问此对象(没被 GC Roots 引用连接),则调用 finalize。
除非是由于其他已准备就绪的对象或类的 finalize 所采取的行动。finalize 方法可以采取任何行动,包括使此对象再次可用于其他线程;这句话的核心是解释 finalize 方法的一个关键特性:对象可以在自己的 finalize 方法中'复活'自己,或者复活其他对象。
然而,finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作。例如,表示输入/输出连接的对象的 finalize 方法可能会在对象被永久丢弃之前执行显式 I/O 事务以断开连接。
Java 虚拟机对任何给定对象调用 finalize 方法的次数都不会超过一次。
使用 finalize 可能会导致安全性、性能和可靠性方面的问题。其中虚拟机并不承诺 finalize 的内容会被执行完成,如果一个对象的 finalize 方法执行非常缓慢,或者发生了死循环(例如 while(true)),将导致 F-Queue 队列一直堆积,最终可能导致整个内存回收子系统崩溃,甚至 OOM(内存溢出)。
两次标记
当满足以下两种需求时,才会触发两次标记(如果 finalize 没被重写,那么在第一次 GC 标记后就会直接给对象空间清除了):
- Java 堆中的对象不被 GC Roots 引用连接
finalize被重写
第一次标记
- Step 1(可达性分析): 发现对象不可达。
- Step 2(第一次标记/筛选): 判断该对象是否有必要执行
finalize()方法。- 如果对象没有覆盖
finalize()方法,或者finalize()已经被虚拟机调用过,那么虚拟机视为'没有必要执行'。—— 这种情况下,对象直接被判定为'死亡',等待回收,不会进入 F-Queue。 - 只有'有必要执行'的对象,才会被放入 F-Queue。
- 如果对象没有覆盖
接下来会由虚拟机自动建立的、低调度优先级的 Finalizer 线程去调用队列中对象的 finalize 方法,对象如果在 finalize 中将自己赋值给一个变量(即赋值给 GC Roots 中的一个对象)。
第二次标记
第二次标记所做的事情是看队列中的对象是否自救成功。
当对象自救后,垃圾处理器在进行二次标记处理时就会发现该对象还存在引用,则会取消对其的空间释放。
public class Zombie {
static Zombie savedInstance;
// 一个存活的静态引用
@Override
protected void finalize() throws Throwable {
System.out.println("Finalize called!");
savedInstance = this; // 关键行动:将即将被销毁的当前对象赋给静态变量
// 对象现在'复活'了!
}
public static void main(String[] args) throws InterruptedException {
Zombie z = new Zombie();
z = null; // 删除唯一引用,z 变得不可达
// 建议 GC,第一次
System.gc();
Thread.sleep(1000);
if (savedInstance != null) {
System.out.println("Zombie object is alive!");
} else {
System.out.println("Zombie object is dead.");
}
// 再次尝试回收(注意:一个对象的 finalize 最多被 JVM 调用一次)
savedInstance = null;
System.gc();
Thread.sleep(1000);
System.out.println("Process finished.");
}
}
从上面的两次标记过程我们不难发现,标记实际也可以被称为处理,但是只有当 finalize 被重写后的对象才存在两次标记。
比喻
想象这是一个超级大的,人人自危的修仙世界:
- GC Roots - 各个宗门,它会根据你的作用来决定你是否是'宗门弟子'(GC Roots 引用)
- Java 堆 - 存在洪荒野兽的修仙世界
- 垃圾收集器 - 一个规则怪兽,某个时间中,他会杀死所有没被宗门庇护的修仙者
- finalize 方法 - 修仙者的'保命符',可能是能让修仙者直接成为某个宗门的弟子,(但是只能使用一次)


