说实话,开源社区里面有很多人都在讲: 可达性算法中 JVM 会进行两次标记,第一次会标记所有对象,并找到继承实现了 finalize() 方法的对象,并查看该对象是否存在"自救",这些内容都与《深入理解 Java 虚拟机》(后文简称为 '书')中 3.2.4 生存还是死亡?这一小节存在出入,或者说几乎所有的博客都是通过阅读这一小节然后得到令人一知半解的回答,整个逻辑有点混乱,前后不搭,所有我打算总结一下
说明:虽然主要讲堆内存,但方法区也有 GC,只是条件更为苛刻(所有实例被回收、ClassLoader 被回收等)。
首先会先说明可达性分析算法的规则
其次会了解一下finalize 方法,包括它的作用时机和作用次数
最后再来说一下 JVM 垃圾收集器根据 可达性分析算法 和 Java 对象机制怎么"两次标记",怎么回收 Java 堆中的对象空间
最后的最后我会按照我的理解来完全比喻一次
可达性分析算法
垃圾收集器一般采用的是可达性分析算法,内容是: 如果某个对象没有通过引用链连接到 GC Roots 就表明这个对象不可达,所以会被判定为可回收对象
GC Roots 是一个数据结构,里面可以包含很多的内容,我们挑选其中一个:对象是否被栈内 reference 类型强引用
也就是在方法中 new 出来的对象会被强引用连接,如下图所示

根据 JVM 内存区域的分配我们可知,new Student(); 是在 Java 堆中开辟了一块内容,用于存放 Student 相关内容,(在这里需要用到方法区我没有详细标出)
Java 中取消了指针,但事实上对象也就相当于指针作用,例如变量 s 它的内容就是刚刚创建在 Java 堆上对应对象的数据
如果将 s 设置为 null 则代表将指针指向的位置设置为空,如下图

那么在这个时候 Java 堆刚刚开辟的空间 Student 就会被可达性算法检测为"GC Roots 到该对象不可达"
finalize 方法
首先,finalize 是所有类中都存在的一个方法,它只有在 GC 过程中会被触发使用,而且它只会被使用一次, 并且它已经被弃用了
在书中说到:
它并不能等同于C和C++语言中的析构函数,而是Java刚诞生时为了使传统CC程序员更容易接受 Java 所做出的一项妥协,它的运行代价高昂、不确定性大、无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。有的教材中描述 finalize 适合做"关闭外部资源"之类的清理性工作,这完全是对 finalize 用途的一种自我安慰。finalize 能做的使用try-finally或者其他方式都可以做得更好更及时所以笔者建议大家完全可以忘掉Java语言的这个方法
同时在 Java 源码中也如是说到:
A subclass overrides the finalize method to dispose of system resources or to perform other cleanup.
子类重写finalize方法以处置系统资源或执行其他清理。
The general contract of finalize is that it is invoked if and when the Java virtual machine has determined that there is no longer any means by which this object can be accessed by any thread that has not yet died, except as a result of an action taken by the finalization of some other object or class which is ready to be finalized.The finalize method may take any action, including making this object available again to other threads; the usual purpose of finalize, however, is to perform cleanup actions before the object is irrevocably discarded. For example, the finalize method for an object that represents an input/output connection might perform explicit I/O transactions to break the connection before the object is permanently discarded.
finalize 的一般约定是:如果 Java 虚拟机确定任何尚未死亡的线程都无法再访问此对象(没被 GC Roots 引用连接),则调用 finalize。
除非是由于其他已准备就绪的对象或类的 finalize 所采取的行动。finalize方法可以采取任何行动,包括使此对象再次可用于其他线程;(这句话的核心是解释 finalize 方法的一个关键特性:对象可以在自己的 finalize 方法中"复活"自己,或者复活其他对象。)
然而,finalize 的通常目的是在对象被不可撤销地丢弃之前执行清理操作。例如,表示输入/输出连接的对象的finalize方法可能会在对象被永久丢弃之前执行显式I/O事务以断开连接。
The finalize method is never invoked more than once by a Java virtual machine for any given object.
Java虚拟机对任何给定对象调用finalize方法的次数都不会超过一次。
The use of finalization can lead to problems with security, performance, and reliability.
使用 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 方法 - 修仙者的"保命符",可能是能让修仙者直接成为某个宗门的弟子,(但是只能使用一次)