Effective Java 读书笔记(7)避免finalizer

7.Avoid finalizers

大意为 避免 ”终结者“(finalizer)

Finalizers是不可预料的,经常是危险的并且经常是没有必要的

对于Finalizers他们的使用可能会造成错误的产生,糟糕的性能以及移植性的问题,当然Finalizers有着一些有用的优点,我们会在后续介绍这些,但是作为首要的规则,你应该避免finalizers

C++程序编写者们被警告不要去如同C++析构对于Java的模拟来考虑finalizers,在C++之中,析构函数是经常被用来作为回收对象间关联资源的方法,作为构造函数的反面,但是在Java之中,垃圾回收收集器会在一个对象变得不可达的时候回收它的相关资源,对于程序员这部分来说并不需要特别地去添加,C++的析构也通常被用来回收其他的非内存资源,Java中try-finally的块就是经常用作这个目的。

一个对于finalizers的缺点就是并没有什么保证他们会及时被执行,直到一个对象变得不可达,finalizer可以很久之后才执行,这就意味着你应该对于时间严格的任务就不用考虑finalizer了,举个例子,有一个严重的错误取决于一个关闭文件的finalizer,因为打开文件的描述是一个有限制的资源,如果太多文件需要打开由于JVM执行finalizer比较迟缓,由于再也无法打开文件 一个程序可能就会挂掉了

哪一个finalizer会被执行的及时性主要是是垃圾回收器算法的一个功能,这个算法在JVM的实现中变化很大,一个依赖于finalizer的及时性执行的程序的表现可能同样变化,很有可能的是这样的一个程序在你测试的JVM上运行良好,而在你最重要的顾客喜欢的JVM上失败了

延迟的finalization并不仅仅是理论上的一个问题,为一个类提供一个finalizer,在极少数情况下,对于该实例的任意的回收会延迟,一个部门在调试一个运行了很久的GUI应用时突然该应用奇怪地挂掉了并且产生了一个内存溢出错误(OutOfMemoryError /OOM),通过对这个程序挂掉地时候的分析来看,这个应用有着成千上万的图像对象在它的finalizer队列中,这些finalizer正在等待被finalized掉或者回收掉,不幸运的是,这个finalizer线程的优先级相较于其他线程来说实在是比较低,故那些对象并没有被finalize掉直到他们的finalize线程足够优先了

语言上的规范使得对于哪个线程来执行finalizer没有保证,故并没有便捷的方法来避免这个问题,所以避免使用finalizer还是比较实用的

不仅仅语言上的规范对finalizer的及时执行提供不了保证,它同时也无法保证这些finalizer会全部被执行,一个程序不执行finalizer在某些已经不可达了的对象上而终止了的情况是绝对有可能存在的,故我们应该永不依赖于finalizer去更新一些严格持久的状态,举个例子,利用finalizer来在分享了的资源上释放一个持久的锁,比如一个数据库,这样的确是个好方式莱斯利的整个分布式系统逐渐停止掉~

不要被System.gc和System.runFinalization所诱骗到,他们可能增加了fina这两个方法lizer被执行的概率,但是他们并不能保证执行,唯一的能够保证finalization的方法就是System.runFinalizersOnExit还有它的邪恶的双胞胎 Runtime.runFinalizersOnExit,这两个方法有着致命的缺陷[线程终止]并且已经被弃用了

如果你还觉得避免finalizer这并不是特别令人信服的话,这里有其他的一些值得考虑的情况:

  • 如果一个没有捕获到的异常被抛出在finalization的时候,这个异常会被忽略,并且那个对象的finalization会中止
  • 没有捕获到的异常会使对象在一个污浊的状态之中,如果其他线程其他使用这样的一个污浊状态的对象,可能会导致任意的非确定性后果

更为普通地来说,一个没有捕获的异常会终止线程并且把堆栈跟踪打印出来,但是如果在finalizer里面的话,最惨的结果就是什么也没有打印出来,甚至是一个警告

还有一件事,使用finalizer有一个十分严重的惩罚性的表现,在我的机子上,创建一个简单的对象需要花费5.6ns,如果加上finalizer就变成了2400ns,换句话说,这就是430倍的增长

那么,如果不使用finalizer我们该如何去处理一个含有封装资源并且需要被终止的类呢,比如文件或者线程,解决的办法就是提供一个显式的终止方法,并且要求这个类的用户对于每个这样的已经不需要了的实例去调用这个方法,值得提的一件事就是我们必需时刻知道它是否被终止了,这个显式终止方法必需写在一个private的域里 面,当这个对象不再有效,其他的方法必需检查这个域并且当这个类被终止后又被调用了的时候抛出一个IllegalStateException的异常

典型的关于显式的终止方法的例子就是InputStream,OutputStream和java.sql.Connection里面的close方法,其他的例子比如java.util.Timer里面的cancel方法,这个方法在对于使关联Timer实例的线程自身轻柔地终止上表现出必要的状态转变

java.awt包括Graphics.dispose和Window.dispose的例子,这些方法经常被忽略,从而表现出可怕的结果

一个相关的方法就是Image.flush,这个方法解除了所有关于Image实例相关的资源的分配,但是对于该实例保留了一个仍然可用的状态,如果需要的话会再一次重新分配资源

显式终止方法在try-finally结构的组合上被特别地使用来保证终止,在finally的块中调用显式的终止方法会使得它会被执行即使当这个对象正在被使用的时候一个异常被抛出

// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
     foo.terminate(); // Explicit termination method
}

所以,到这了你还觉得finalizer好?这可能有两种的使用,一种是在一个对象的拥有者忘记调用显式终止方法的时候作为一个”安全网“,即使对于这个finalizer会不会及时地调用并没有保证,但是有总好过没有,在这种情况下finalizer如果发现资源还没有被终止地话必需log一个警告出来,作为bug的提示,如果你考虑编写这么一个安全网finalizer的话,对于一些额外的维护所造成的代价你也必须考虑一下

有这么四个类可以作为显示终止方法模式的例子(InputStream,OutputStream,Timer和java.sql.Connection ),这四个类有着当他们的终止方法不起作用或者没有调用的时候起到安全网作用的finalizer,不幸运的是这些finalizers并不会log一些警告,这样的警告在这个API发布后不能被通用地添加,对于程序员比较不爽

第二种对于finalizer合法的使用就是关于本地对等体(Native Peers)的,一个native peer就是一个native 对象通过native方法来代表一个普通的对象,由于这个native peer并不是普通的对象,垃圾回收器当Java peer回收的时候不知道也不会去回收它,一个finalizer对于完成这个任务是合适的,假定这个native peer没有严格的资源的话。如果native peer有着必须要被及时终止的资源的话,这个类应该使用一个显式的终止方法,正如上面所描述的那样。只要一旦需要释放这些严格的资源的话终止方法就应该执行。当然这个显式的终止方法也可以是一个本地方法,或者是可以被本地方法调用的

需要注意的一点就是”finalizer chaining(链接)“并不会自动地表现。如果一个类有一个finalizer并且一个子类重写了这个finalizer,这个子类必须人为手动地调用父类地finalizer,你应该在子类的try块里面去终结在finally里面调用父类的finalizer,来保证父类的finalizer也能够执行,即使子类的finalization里面抛出了一个异常,反之亦然。下面给出相应的代码块

// Manual finalizer chaining
@Override protected void finalize() throws Throwable {
try {
     ... // Finalize subclass state
} finally {
super.finalize();
}
}

如果一个子类继承于一个父类,并且重写父类的finalizer,但是忘记调用了,父类的finalizer就永远不会被调用。对于预防这样的一个粗心而且恶意的子类是有可能的,你只需要为所有要被finalize的对象创建一个额外的对象。并不是去放一个finalizer在需要finalization的类里面,而是把这个finalizer放到一个匿名类里面,这个匿名类的主要任务就是finalize它的封装的实例。匿名类的一个单一的实例,叫做finalizer guardian,每当创建一个封装类的实例的时候就会创建出来。这个封装的实例储存对它的finalizer guardian的主引用在一个private的域里面,故finalizer guardian作为一个封装实例来说对于同时的finalization可以变得可选。当这个guardian被终结的时候,它就会执行封装实例所需的finalization的活动,就像finalizer就是一个在这个封装实例里面的方法

// Finalizer Guardian idiom
public class Foo {
// Sole purpose of this object is to finalize outer Foo object
private final Object finalizerGuardian = new Object() {
@Override protected void finalize() throws Throwable {
     ... // Finalize outer Foo object
}
};
... // Remainder omitted
}

需要说明的一点是,给出的Foo类没有finalizer(继承于Object会有一个不重要的finalizer,在这里可以忽略),所以对于一个子类的finalizer有没有调用父类的finalizer的方法已经变得无关紧要了,这种技术对于每一个非final的拥有一个finalizer的public类都应该被考虑一下

总结一下,不要使用finalizers除非你想构建一个安全网或者是终止非严格的本地资源。当你使用finalizer在那些罕见的实例之中,记得调用父类的finalize。如果你想当成安全网来使用的话,记得去log那些对于finalizer无效的使用。最后,使用finalizer guardian,可以使finalization能够在子类没有或者忘记调用父类的finalize的情况依然能够执行。

你可能感兴趣的:(Effective Java 读书笔记(7)避免finalizer)