为什么尽量不要Override finalize方法

缘起

我们在java.lang.Object中的约莫10个方法中能看到 finalize方法的身影. 但是java官方并不推荐 Override此方法. 这是为什么呢?

分析

基于以下三点原因

  1. finalize方法中可能会导致对象复活.
  2. finalize方法的执行时间是没有保障的, 完全取决于GC线程,极端情况下,如果不发生GC的话,finalize方法将不会被执行. 所以释放资源的代码不要放在finalize方法中, 而是推荐放在 try…catch…finally 中.
  3. 一个耗时较长的finalize方法将会严重影响GC的性能.

要理解第一点,首先必须要明确什么叫做复活? 这就必须要理解java中对象的状态. java对象的状态按缘起到缘灭的顺序有以下七种

  1. 创建阶段( Created )
  2. 应用阶段( In Use )
  3. 不可见阶段( Invisible )
  4. 不可达阶段( Unreachable )
  5. 收集阶段( Collected )
  6. 终结阶段( Finalized )
  7. 对象空间重分配阶段( De-allocated )

创建阶段

为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切 换到了应用阶段

应用阶段

对象至少被一个强引用持有着。

不可见阶段

当一个对象处于不可见阶段时,说明应用程序本身不再持有该对象的任何强引用,虽然该对象的强引用可能仍然是存在着的。例如JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为” GC root ”

不可达阶段

对象处于不可达阶段是指该对象不再被任何强引用所持有。

收集阶段

当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize() 方法,则会去执行该方法的终端操作。 即收集阶段是执行finalize方法的阶段.

终结阶段

当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

对象空间重新分配阶段

垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

好了,所谓的复活指的就是在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”(即没有走 终结阶段,而是跳回了应用阶段)。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。

具体例子参见DEMO 【1】

要理解第二点和第三点原因就要了解一下GC的过程了. 在上述收集阶段,各个进入收集阶段并且覆写了finalize方法的对象(如果没有覆写的话,则不会进到这里)的finalize方法其实是被FinalizerThread线程处理的.事实上, 每个即将被回收并且Override了finalize方法的对象在收集阶段都会被加入到FinalizerThread的执行队列中. 这里还是有必要画一下类图,免得迷惑.

即Finalizer中有属性queue——一个引用队列,而且Finalizer有一个内部类——FinalizerThread,这个FinalizerThread线程类会使用这个ReferenceQueue queue. 而队列中每一个元素都是一个Finalizer对象(根据上面类图,这就是一个引用对象,所以才叫引用队列嘛~, 注意,这个队列就是【1】中四大引用绑定的队列(虽然也是引用队列),因为queue是四大引用的父类Reference的属性啊.). 而队列的实现是通过双向链表——何以为证? Finalizer有属性

1
2
3
private Finalizer
next = null,
prev = null;

有点数据结构知识的同学都知道,这就是链表前一个元素和后一个元素,以及java.lang.ref.Reference中有一个属性(java.lang.ref.Reference 是Finalizer的父类)

1
private T referent;

这个referent属性就是双向链表的当前元素的一个属性(虽然该属性私有不能被子类继承,但是父类提供了get方法对其进行发布. 所以子类都能拿到父类中的这个属性,不同子类实例的父类是不一样的,因为不同子类实例创建的时候都会创建父类实例,即调用父类的构造器). 而这是一个强引用,它引用的就是即将被GC的对象啊~

这是非常重要的一点!!!由于对象在收集阶段被Finalizer的referent字段强引用, 并加入了queue中, 这意味着,收集阶段的对象又会暂时变得”可达~”,所以此时不能被GC!!!(下图画圈圈的话就是在说这一点) 如果finalize方法写的比较耗时的话,则将导致这些队列中的对象长期积压在内存中,可能会导致OOM. 下图展示了这个工作过程

参考

【1】https://yfsyfs.github.io/2019/06/28/Java%E4%B8%AD%E7%9A%84-%E5%BC%BA%E5%BC%95%E7%94%A8%E3%80%81%E8%BD%AF%E5%BC%95%E7%94%A8%E3%80%81%E5%BC%B1%E5%BC%95%E7%94%A8%E3%80%81%E8%99%9A%E5%BC%95%E7%94%A8/

DEMO

【1】https://github.com/yfsyfs/backend/tree/master/CanRelive