Java 垃圾回收 - 使用 finalize 方法拯救自己

在可达性分析算法中不可达的对象,也并非是“非死不可”的,一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize ( ) 方法。当对象没有覆盖 finalize ( ) 方法,或者 finalize ( ) 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己( this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。从下面我们可以看到一个对象的 finalize() 被执行,但是它仍然可以存活。

FinalizeEscapeGC.java

public class FinalizeEscapeGC {
  private static FinalizeEscapeGC SAVE_HOOK = null;

  @Override
  protected void finalize() throws Throwable {
    super.finalize();
    SAVE_HOOK = this;
    System.out.println("finalize method executed !");
  }

  public static void main(String[] args) throws InterruptedException {
    SAVE_HOOK = new FinalizeEscapeGC();

    /*第 1 次回收失败,对象成功拯救自己*/
    tryCollectHook();

    /*第 2 次回收成功*/
    tryCollectHook();
  }

  private static void tryCollectHook() throws InterruptedException {
    SAVE_HOOK = null;
    System.gc();
    /*由于 finalize 方法优先级较低,这里暂停 5 秒以等待它*/
    Thread.sleep(5000);
    if (SAVE_HOOK == null) {
      System.out.println("SAVE_HOOK is dead :(");
    } else {
      System.out.println("SAVE_HOOK is still alive :)");
    }
  }
}

程序运行结果:

finalize method executed !
SAVE_HOOK is still alive :)
SAVE_HOOK is dead :(

从运行结果可以看出,SAVE_HOOKfinalize() 方法确实被 GC 收集器触发了,并且在第一次垃圾收集时成功逃脱了,第二次没能逃脱成功的原因是任何一个对象的 finalize() 方法只会被系统自动调用一次,如果面临下一次回收,它的 finalize() 方法不会被再次执行,因此就不能从第二次垃圾回收逃脱了。

finalize() 方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,建议大家慎重使用,如果需要一些“关闭外部资源”之类的工作,可以使用 try-finally 来实现。

说明

文章摘自《深入理解Java虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。