finalize

想要去了解 finalize 的最初起因是想去了解下 java.lang.ref 下的三个 Reference 类。这三个类在实践中用的比较少,但却是面试中经常会问到的。[1] 在理解这个三个类时需要理解对象的可达性(reachable),可达对象就是所谓的强引用,也即程序中通常使用的引用。这种对象可以被任何活线程访问,这里的活线程指除了 finalizer 线程外的其它运行中的线程。当对象不再可达时,就成为了 GC 的候选对象。此时,如果对象定义了 finalize 方法,这个方法被称为 finalizer。GC 在回收实现了 finalize 方法的对象前会调用此方法,然后才是回收内存,这给实现此方法的对象提供了一个在被回收前来释放外部资源,具体可能是线程池、socket 连接、文件句柄等。[2]

但 finalize 方法是有问题的。首先,此方法的调用时机是未知的,意味着这些对象或资源可能会占用不确定的时间后才会被释放,这在 burst 时会是一个很严重的性能瓶颈。其次,此方法调用的顺序是不可知的,甚至可能是并发的,所以必须小心使得实现了 finalize 方法的对象间不要有依赖,不要访问相同的资源。如果确实有依赖关系,那就让其中一个对象的 finalize 方法来调用其它对象的方法。最后,如果在 finalize 方法中抛出了任何异常都不会被应用程序感知,它会静默地失败然后中止执行。

实现细节

实现了 finalize 方法地对象是特殊公民。当方法 GC 时,这些对象需要构建一个 java.lang.ref.Finalizer 来 wrapper 此对象,Finalizer 继承自 Reference 类。背后所做的事与 Reference 是一样的,最终这些对象在被 JVM 标记为 pending 时会被添加到 ReferenceQueue 中,Finalizer 线程会从 ReferenceQueue 中不断 remove 元素并调用其 finalize 方法。整个工作完成以后,对象才能被 GC 回收。[3]Finalizer 线程是一个优先级非常低的线程,如果有大量实现 finalize 方法的对象可被回收,队列中就会堆积许多对象,从而占用大量内存。如果 finalize 方法需要做大量工作,结果将更加严重。所以不要在 finalize 方法中做耗时操作。有 benchmark 表明 finalize 方法的存在会导致 50 倍效率的下降。[4]

如果去看 java.lang.ref.Reference 中的代码会发现不容易读懂,原因是里边除了 Java 层面所做的工作外还有 GC 层面进行 pending 对象标记,以及 GC 针对 WeakReference SoftReference Finalizer PhantomReference 引用所作的不同工作。假如需要继续跟踪下去,就需要去查阅 GC 的源码了。

为什么需要了解 finalize ?

虽然在项目中应该尽量避免使用 finalize,但 finalize 不是一无是处。finalize 可以作为安全网(safety-net)来使用。所有的资源都应该在使用完毕后及时 close 调用,实现了 AutoCloseable 接口的类,在使用 try-with-resources 时,编译器就产生 close 调用。假如在某些时候忘记了关闭资源,而对象已经被回收了,那就造成了资源泄露。实现 finalize 就是一个合理的选择,它会帮助在对象回收时进行关闭(前提依然是在使用完后尽快调用 close)。这在 Java 标准库中已经多次遇到了,比如:ThreadPoolExecutor FileInputStream FileOutputStream 中都可能找到 finalize 方法。[4]

Reference

[1] 杨晓峰,第4讲 | 强引用、软引用、弱引用、幻象引用有什么区别?
[2] jls 12.6
[3] Ram Lakshmanan,What happens behind the scenes for finalize() method?
[4] Joshua Bloch, Item 8: Avoid finalizers and cleaners, Effective Java.3rd

Leave a Reply

Your email address will not be published. Required fields are marked *