天天看点

《Effective Java》读书笔记06--避免使用终结方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。

一、终结方法VS析构器

熟悉C++的都知道,析构器是用来回收一个对象所占用资源的常规方法,是构造器所必需的对应物。在JAVA中,当一个对象变得不可达时,垃圾回收器会回收与该对象关联的存储空间,这不需要我们操心。对于非内存资源的回收,C++析构器是可以管理的,而JAVA的垃圾回收器是不会管理这些非内存资源的,我们通常使用try-finally块来显式管理这些资源,比如文件操作,socket连接等。

二、终结方法的劣行

1、行为不稳定

终结方法不能保证被及时地执行,有时甚至根本不会被执行。从一个对象变成不可到达开始,到它的终结方法被执行,所花费的时间是任意长的。如果在终结方法中执行文件关闭操作,很有可能造成程序因为不能打开文件而出现运行错误,因为终结方法执行时间是任意的,而系统文件打开数是一定的,当系统文件打开数达到最大时,程序就会抛出Open too many files异常,这是系统级错误,所以不能依靠终结方法来处理这些有限资源的释放。另外,也不能依赖终结方法来更新重要的持久状态。例如,依赖终结方法解放共享资源(比如数据库)上的永久锁,很容易让整个分布式系统垮掉。

2、可移植性问题

及时执行终结方法是垃圾回收算法的一个主要功能,而每个JVM实现中,垃圾回收算法是不一样的,这就导致终结方法在不同JVM实现下表现的差异,这种差异可能带来灾难性影响。

3、性能损失

使用终结方法销毁对象需要依赖于JVM的垃圾回收算法调度,这样必将造成销毁对象耗时更多,降低性能。

三、终结方法的诱惑

对于初学者来说,特别是不了解JVM垃圾回收思想的同学,比如我,经常会使用System.gc()来处理一些逻辑,这不是最佳编程实践,我们最好不要这样做。System.gc()和System.runFinalization()这两个方法确实能够增加终结方法被执行的机会,但它们并不能保证终结方法一定会被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit和Runtime.runFinalizerOnExit,但这两个方法都有致命的缺陷,已经被废弃了,听着很吓人哈。

四、终结方法的合理使用场景

1、当对象的所有者忘记调用显示终止方法时,终结方法可以充当"安全网"。

显示终止方法是提供一个显式地终止方法,并要求该类的客户端在每个实例不在有用的时候调用这个方法,比如InputStream、Outputstream和java.sql.Connection的close方法。java.util.Timer的cancel方法,它会执行必要的状态改变,使得与Timer实例相关联的线程温和得终止自己。Image.flush会释放所有与Image实例相关联的资源,但该实例仍然处于可用的状态,如果有必要的话,会重新分配资源。

显式终止方法所在类的实例必须记录自己是否已经被终止,这样做主要是为了防止多次调用终止方法。当类实例被终止之后,再调用终止方法会抛出IllegalStateException异常(这个需要类实现者实现,只需判断类终止标志是否显式该对象已经无效,无效则抛异常)。

使用终结方法充当"安全网"虽然不能保证终结方法会被及时调用,但在客户端无法通过调用显式的终止方法来正常结束操作的情况下,迟一点释放关键资源总比永远不释放要好,此时最好将资源未释放的情况记录到日志中,以便修复bug。

2、通过本地方法与本地对象交互时

Java对象通过本地方法将某操作委托给一个本地对象时,如果本地对象不拥有关键资源,则可以在终结方法中执行这项任务。如果本地对象拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法,在该方法中完成所有必要的操作以便释放关键资源。终止方法可以是本地方法,也可以调用本地方法。

五、终结方法守卫者

终结方法守卫者是用来终结它的外围实例。因为,“终结方法链”不会被自动执行。如果父类有终结方法,并且子类覆盖了终结方法,那么子类的终结方法必须手工调用超类的终结方法。

protected void finalize() throws Throwable{
try{
 ....//Finalize subclass state
}finally{
super.finalize();
}

}
           

这样可以保证: 即使子类的终结过程抛出异常,超类的终结方法也会得到执行。反之亦然。

如果子类实现者覆盖类超类的终结方法,但忘了手工调用终结方法(或者有意选择不调用超类的终结方法),那么超类的终结方法将永远不会被调用到。终结方法守护者就是为了防止此类状况的发生,做法是为每个将被终结的对象创建一个附加的对象,并把终结方法放到这个匿名的对象中,由于外围实例会在它的私有实例域中保存一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程(垃圾回收器在回收外围实例时会将它关联的无用对象回收),而这个终结行为正是外围对象所期望的。

//Finalizer Guardian idiom
public class Foo{
   private final Object finalizerGuardian = new Object(){
@Override protected void finalize() throws Throwable{
  ...//终结外围实例
}
};
...//Remainder omitted
}
           

注意:公有类Foo并没有终结方法(除了它从Object继承过来的一个无关紧要的之外),所以子类的终结方法是否调用super.finalize并不重要。对于每一个带有终结方法的非final公有类,都应该考虑使用终结方法守卫者模式。

六、最佳编程实践

1、除非作为安全网,或为了终止非关键的本地资源,否则不要使用终结方法。

2、如果使用终结方法,就要记住调用super.finalize

3、如果需要把终结方法与公有的非final类关联起来,需考虑使用终结方法守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。