Java性能优化(6):避免使用终结函数

终结函数通常是不可预测的,常常也是很危险的,一般情况下是不必要的。使用终结函数会导致不稳定的行为、更差的性能,以及带来移植性问题。当然,终结函数也有其可用之处的但是作为一个优秀的programer,应当避免使用终结函数。
C++程序员被告知“不要把终结函数当做C++中析构函数的对应物”。在C++中,析构函数是回收一个对象所占资源的常规方法,是构造函数所必须的对应物。在Java语言中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要程序员做专门的工作。C++的析构函数也可以被用来回收其他的非内存资源。在Java程序语言设计中,一般用try-finally块来完成类似的工作。

终结函数并不能保证会被及时地执行,从一个对象变得不可到达开始,到他的终结函数被执行,这段时间的长度是任意的、不确定的。这意味着,时间关键的任务不应该由终结函数来完成。例如,由终结函数来关闭一个已经被打开的文件,这是严重错误,因为已打开文件的描述符是一种很有限的资源。由于JVM会延迟执行终结函数,所以大量的文件会保留在打开状态,当一个程序不能再打开文件的时候,它可能会运行失败。
及时地执行终结函数正是垃圾回收算法的一个主要功能,这种算法在不同的JVM实现中会大相径庭。如果一个程序依赖于终结函数被执行的时间点,那么这个程序的行为在不同JVM中运行的表现可能就会截然不同。一个程序在你测试用的JVM平台上与运行的非常好,而在你最重要的顾客那里的JVM平台上根本无法运行,这完全是有可能的。
延迟终结过程并不只是一个理论问题。在很少见的情况下,为一个类提供一个终结函数,可能会随意地延迟其实例的回收过程。一位同学最近在调试一个长时间运行的GUI应用程序的时候,该应用程序莫名地由于OutOfMemoryError错误而死掉。分析表明,该应用程序死掉的时候,其终结函数队列中有数个图形对象正在等待被终结和回收。不幸的是,执行终结函数的线程的优先级比该应用程序的其他线程要低很多,所以,图形对象被终结的速度达不到它们进入队列的速度。JLS(JAVA语言规范)并不能保证哪个线程会被执行终结函数,所以除了不使用终结函数外,并没有一个可移植的办法能够避免这样的问题。
JLS不仅不保证终结函数会被及时的执行,而且根本就不保证它们会被执行。当一个程序终止的时候,其中某些已经无法访问的对象上的终结函数根本就没有被执行。这是完全有可能的,结果是,我们不应该依赖一个终结函数来更新关键性的永久状态。例如,依赖于终结函数来释放一个共享资源上的永久锁,是让你整个分布式系统垮掉的好办法。
不要被System.gc和System.runfinalization这两个方法所诱惑,它们确实增加了终结函数被执行的机会,但是它们并不保证中终结函数一定会被执行。唯一声称保证终结函数被执行的方法是System.runFinallizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了。
当你并不确定是否应该避免使用终结函数的时候,这里还有一个值得考虑的情形:如果一种未被捕获的异常会在终结过程中被抛出来,那么这种异常可以被忽略。并且该对象的终结过程也会终止,未被补货的异常会是对象处于破坏的状态,如果另一个线程企图使用这样一个被破坏的对象,则任何不确定的行为都有可能发生。正常情况下,一个未被捕获的异常将会使线程终止,并打印出栈轨迹,但是,如果异常发生在一个终结函数之中,则情形不会这样——甚至连警告都不会打印出来。
那么,如果一个类封装的资源确实需要回收,我们该怎么办才能不再需要编写终结函数呢?只需要提供一个显示的终止方法,并要求该类的客户在每个实例不再有的时候调用这个方法。一个值得提及的细节是,该实例必须记录下自己是否已经被终止了:显示的终止方法必须在一个私有记录下“该对象已经不在有效了”,其他的犯法必须检查这个域。如果在对象已经被终止之后,这些方法被调用的化,那么它们应该抛出IllegalStateException异常。
显示终止方法的一个典型例子是InputStream和OutputStream上的close方法。另一个例子是java.util.Timer上的cancel方法,它执行必要的状态改变,使得与Timer实例相关的该线程温和地终止自己,其他的例子还包括java.awt中的Graphics.dispose和Window.dispose。这些方法通常由于性能不好而不被人们关注,一个相关的方法是Image.flush,它会释放所有与Image实例相关的资源,但是该实例任然处于可用的状态,如果有必要的话,会重新分配资源。
显示的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显示的终止方法可以保证:及时在对象被使用的时候有异常被抛出来,该终止方法也会被执行:

Foo foo = new Foo(...);
try
{

}
finally
{
    foo.terminate();
}

那么终结函数有什么好处呢?它们有两个合理的用途。第一种用途是,当一个对象的所有者忘记了调用前面段落中建议的显式终止方法的情况下,终结函数可以充当“安全网(safetynet)”。虽然这样做并不能保证终结函数会被及时调用到,但是在客户无法通过调用显式的终止方法来正常结束操作的情况下,迟一点释放关键资源总比永远不释放要好。上文中,作为显示终止方法模式的例子而提到的一个类(InputStream、OutputStream和Timer)也都有终结函数,在终止方法未能被调用的情况下,这些终结函数被用来充当安全网。
终结函数的第二种合理用途与对象的本地(native peer)有关。本地对等体是一个本地对象(native object),普通对象通过本地方法委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的普通对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键资源的前提下,终结函数正是执行这项任务最合适的工具。如果本地对等体拥有必须被及时终止的资源,那么该类应该具有一个显式的终止方法,如前所述。终止方法应该完成必要的工作以便释放关键的资源。终止方法可以是一个本地方法,或者它也可以调用本地方法。
值得注意的很重要的一点事,”终结函数链” 并不会被自动执行。如果一个类有一个终结函数,并且一个子类改写了终结函数,那么子类的终结函数必须要手工调用超类的终结函数。你应该在一个try块中终结子类,并且在对应的finally块中调用超类的终结函数。这样做可以保证,即使子类的终结过程抛出一个异常,超类的终结函数也会被执行,反之亦然。代码示例如下:

protected void finalize() throws Throwable {
        try {

        } catch (Exception e) {
            super.finalize();
        }
    }

如果一个子类实现者改写了超类的终结函数,但是忘了手工调用超类的终结函数,那么超类的终结函数将永远也不会被调用到。要防范这样粗心大意的或者恶意的子类是有可能的,只要为每一个将被终结的对象创建一个附加的对象。不是把终结函数放在要求终结处理的类中,而是把终结函数放在一个匿名的类中,该匿名类的唯一用途是终结其外围的实例。该匿名类的实例单个实例被称为终结函数守卫者,外围类的每一个实例都会创建这样一个守卫者。外围实例在它的私有实例域中保存着一个对其守卫者的唯一引用,所以,终结函数守卫者与外围实例可以同时启动终结过程。当守卫者被终结的时候,它执行外围实例所期望的终结行为,就好像它的终结函数式外围对象上的一个方法一样:

public class Foo {
    private final Object finalizerGuardian = new Object() {
        protected void finalize() throws Throwable {

        }
    };

}

注意,公有类Foo并没有终结函数,所以子类的终结函数式否调用super.finalize并不重要,对于每一个带有终结函数的非final公有类,都应该考虑使用这项技术。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结函数,在这些很少见的情况下,既然你使用了终结函数,那么就要记住调用super.finalize,最后,如果你要把一个终结函数与一个公有的非final类联合起来,那么请考虑使用中终结函数守卫者,以确保即使子类的终结函数未能调用super.finalize,该终结函数也会被执行。

posted on 2015-08-21 12:33  爱你一万年123  阅读(212)  评论(0编辑  收藏  举报

导航