Object.finalize 方法
在 Java 中,一个对象如果不再使用,那么它就会在 JVM 垃圾回收时,进行析构释放该对象占用的内存空间。但如果这个对象持有了一些其他需要进行额外处理的资源(非堆内存资源),那么就得考虑这些资源的释放了。
在 Object 中,有一个方法 finalize
,就是用来做这种操作的。这个方法会在对象被 GC 回收之前被 JVM 调用,一般都使用覆写这个函数来完成一些额外资源的清理工作,例如清理相关的 native 资源或是其他资源(socket、文件)的释放。
在这里推荐大家去看一下这篇文章,不仅讲解了 Object.finalize
方法,还讲解了 Object
中的其他所有方法:一文掌握 Object 类里的所有方法(wait、notify、finalize)
如果子类重写 finalize
这个方法,那么必须调用 super.finalize()
,而且应该使用 try-finally
语法以确保始终调用 super.finalize()
。下面就是一个使用 finalize
进行资源清理的例子:
class FinalizeObj {private long nativePointer;public FinalizeObj() {nativePointer = createNative();}@Overrideprotected void finalize() throws Throwable {try { // 清理工作super.finalize();releaseNative(nativePointer);nativePointer = 0L;} finally {super.finalize(); }}private native long createNative();private native void releaseNative(long nativePointer);
}
这个例子中,在构造方法中创建了 native 底层资源,并在 finalize
方法中释放 native 底层资源。
当一个对象覆写 finalize
方法后,其回收的流程如下:
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了 finalize
方法,若未覆盖,则直接将其回收。否则,若对象未执行过 finalize
方法,将其移动到一个队列里,由一个低优先级线程执行该队列中对象的 finalize
方法。执行 finalize
方法完毕后,这些对象才成为真正的垃圾,等待下一轮垃圾回收。
但是现在想一想,这种进行资源清理的方式好吗?其实是不好的。例如程序员在覆写 finalize
方法时出现一些线程死锁的操作,那么就有可能造成垃圾回收的失败,同事也会产生严重的盐城阻塞问题。而且这个方法被执行的不确定性太大,一个对象从不可达到 finalize 方法被执行,完全依赖 JVM,这就无法保证被对象占用的资源被及时回收。
也是出于以上的原因,这个方法在 Java 9 中已经被标记为 Deprecated
。那替代方案是啥呢?案是使用 java.lang.ref.Cleaner
,这是 Java 9 推出的一个轻量级垃圾回收机制。
java.lang.ref.Cleaner
首先要说的是,这个类是 Java 9 的新特性,但是新特性往往都会带来繁琐的使用方式。但主要包含以下几个步骤
- 创建一个静态内部类继承
Runnable
作为进行清理的线程,在run
方法中进行垃圾清理(必须使用static
以避免持有外部实例的引用) - 创建静态变量
cleaner
初始化为Cleaner.create()
- 在构造方法中创建清理线程,并通过静态变量进行注册
cleaner.register
,并保存返回的cleanable
值 - 实现
AutoCloseable
接口,覆写close
方法,在其中调用cleanable.clean()
说起来比较麻烦,我们将上面那个例子改写一下,使用 Cleaner
来实现:
public class CleanerObj implements AutoCloseable {// 静态变量 cleaner 用于此类的实例的清理工作private static final Cleaner cleaner = Cleaner.create();// 静态内部类,清理线程,用于清理本类的实例private static class CleanRunnable implements Runnable { private final long nativePointerForClean;CleanRunnable(long nativePointer) {//构造方法应该拿到用于清理的所有信息this.nativePointerForClean = nativePointer;}@Overridepublic void run() {//在此进行清理操作releaseNative(nativePointerForClean);}}private long nativePointer; // native资源指针private final CleanRunnable cleanRunnable; // 此对象的垃圾回收线程private final Cleaner.Cleanable cleanable; // 注册时返回的 cleanable 对象public CleanerObj() {//创建底层资源,保存返回的指针this.nativePointer = createNative(); //将底层资源的指针传递给清理线程对象,以便进行清理操作this.cleanRunnable = new CleanRunnable(this.nativePointer);//通过静态变量cleaner注册此对象和清理线程对象this.cleanable = cleaner.register(this, cleanRunnable);}@Overridepublic void close() { //覆写AutoCloseable 的 close 方法cleanable.clean(); //调用 cleaner.register 返回的 cleanable 对象的 clean 方法}private native static long createNative();private native static void releaseNative(long nativePointer);
}
大家注意代码中的注解,已经解释的很清楚。这里再啰嗦一下,就是先创建属于类的静态变量 cleaner
和 清理线程,用于此类的对象的清理操作。而针对每一个对象,都需要通过这两个静态变量,创建清理线程并注册。最后在 AutoCloseable
的 close
方法中,调用 cleanable.clean()
。
实现 AutoCloseable
接口这使得此类在 try-finally
时能够自动调用 close
方法进行清理。如果未调用 close
方法,当这个对象不被任何地方引用时,Cleaner
也将调用清理操作。
我们先使用 try-finally
来进行测试:
try (CleanerObj obj = new CleanerObj()) {// do something with obj.
}
System.gc();
清理的流程如下:
System.out I createNative... thread = main
System.out I close... thread = main
System.out I CleanRunnable.run... thread = main
System.out I releaseNative... thread = main
当我们不使用 try-catch
时:
CleanerObj obj = new CleanerObj();
System.gc();
清理的流程如下:
System.out I createNative... thread = main
System.out I CleanRunnable.run... thread = Cleaner-0
System.out I releaseNative... thread = Cleaner-0
可以看到,两种方式最终都会调用到 releaseNative
方法进行垃圾清理,而不同的仅仅是垃圾处理的线程而已。
相比于 finalize
方法,使用 Cleaner
进行清理操作实在是太麻烦了。但这也是没办法的事情,随着后续 Java 技术的发展,很多传统类结构的设计一定要有所改变,因为现在的程序编写得越来越庞大,同时业务的繁琐程度也越来越高,以及硬件和网络通讯水平的不断发展,程序语言内部的结构升级必然是整个行业的趋势。
最后,这里通过一张图来演示 Java 中对象的生命周期,并结束本篇文章,祝大家升职加薪。