您的位置:首页 > Web前端

【Effective Java】Ch2_创建销毁对象:Item7_避免使用finalize方法

2013-02-19 17:56 267 查看
Finalizer通常是不可预测的、危险的、不必要的。使用finalizer会导致不稳定的行为、低下的性能、以及可移植问题。Finalizer也有其可用之处,本文稍后会做介绍,但是作为一个首要法则,你必须避免使用finalizer。

C++程序员需要注意Java中的finalizer并不类似C++中的析构方法destructor。C++的析构方法是回收一个对象相关的资源的常用途径,是构造方法所必需的对应物;而在Java中,当对象不可达时,与之相关的存储空间就会被垃圾回收器自动回收,不需要程序员做特殊处理。C++的析构方法还被用来回收其他的非内存资源;在Java中通常使用try-finally块来达到这个目的。

finalizer的一个缺点是不能保证它会及时地运行。从一个对象变为不可达,到其finalizer被执行,可能会经过任意长时间。这意味着你不能在finalizer中执行任何注重时间的任务。【例】依靠finalizer来关闭文件就是一个严重错误,因为打开文件的描述符是一个有限资源。JVM会延迟执行finalizer,所以大量文件会被保持在打开状态,当一个程序不再能打开文件的时候,就会运行失败。

finalizer执行的及时性首先是垃圾回收算法的功能,这种算法在JVM的不同实现之间差异很大。如果程序依赖于finalizer执行的及时性,那它的行为在不同JVM上就可能变化多端。很有可能它在你测试的JVM上运行良好,但是在你的重要客户的JVM上却不幸运行失败。

延迟finalizer并不仅仅是一个理论问题。在少数情况下,为类提供finalizer会反复延迟其实例的回收。一位同事调试过一个长期运行的GUI程序,这个程序会因为OutOfMemoryError而神秘地死掉。分析表明,在程序死掉的时候,其finalizer队列里有成千上万的图形对象等待被终结和回收。不幸的是,finalizer线程比其他线程的优先级要低,所以对象的终结速度达不到其进入队列的速度。Java语言规范并不能保证哪个线程会执行finalizer,所以除非禁止使用finalizer,没有简便方法能防止这种问题。

Java语言规范不仅不保证finalizer及时执行;甚至不保证finalizer会被执行。完全有可能出现这种情况,当程序终止时,一些不可达对象的finalizer根本还没有执行。因此,你不能依赖finalizer去更新关键的持久状态。【例】依赖finalizer去释放共享资源(例如数据库)上的持久锁,很容易让整个分布式系统挂掉。

不要被System.gcSystem.runFinalization方法所迷惑,他们会提高finalizer得到执行的几率,但是他们并不保证这一点。唯一声称保证finalizer执行的方法是System.runFinalizersOnExit,以及Runtime.runFinalizersOnExit。这两个方法都有致命缺陷并且都已弃用。

如果你还不相信finalizer应该避免使用,那就考虑另一个情况:如果finalization过程中抛出了一个未捕获的异常,该异常则会被忽略,并且对象的finalization会被终止。未捕获的异常会让对象处于被破坏的状态。如果另一个线程试图使用该被破坏的对象,则会导致不可预料的行为。正常情况下,未捕获的异常会终止当前线程,并打印出对战,但是如果发生在finalizer中则不会!它甚至连警告都不会打印。

还有一点:使用finalizer会导致严重的性能损失。在我的机器上,创建和销毁一个简单对象的实践大约是5.6ns,增加finalizer后时间增加到2400ns。换言之,创建和销毁带有finalizer的对象会慢430倍。

那么,如果类的对象所封装的资源(例如文件或线程)需要终结的话,应该怎么做而不用编写finalizer呢?提供一个显式的终止方法即可,要求客户端在每个实例不在有用的时候 调用该方法。值得提及的一个细节是,实例必须记录他是否已被终止:该显式的终止方法必须在一个私有字段中记录该对象已不在有效,该对象的其他方法必须坚持这个字段,若其在对象被终止后被调用,则抛出IllegalStateException。

【例】显式的终止方法的典型例子是InputStream、OutputStream和java.sql.Connection中的close方法。【例】另一个例子是java.util.Timer中的cancel方法,该方法会执行必要的状态改变,使与该Timer实例相关的线程可以优雅地终止自己。【例】java.awt中的例子包括Graphics.dispose和Windows.dispose,这两个方法由于性能不好而通常被忽视。一个相关的方法是Image.flush,它会释放与Image实例相关的所有资源,但是该实例仍然处于可用状态,如果需要的话会重新分配资源。

public class Timer {
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify();  // In case queue was already empty.
}
}

private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

……
}
}
}
显式的终止方法通常与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发现有未被终止的资源,则必须打印一条警告,表明客户端代码有bug,需要修复。如果你考虑编写这样的安全网finalizer的话,请认真想一想是否值得这种额外的保护。

上面提到的显式终止方法的例子中,有四个类(FileInputStream、FileOutputStream、Timer、Connection)使用了finalizer作为安全网,以防显式终止方法未被调用。不幸的是,这些finalizer都没有打印警告。当API发布后,这种警告一般就不能添加到API了,因为可能破坏已有的客户端代码。

finalizer的第二种合法用处与对象的本地对等体(native peers)相关。所谓本地对等体,是指一个本地对象,普通对象通过本地方法委托给该本地对象。由于本地对等体不是一个普通对象,所以垃圾回收器并不知道它,当其Java对等体被回收时,本地对象并不能被回收。假设本地对等体不占用关键资源的话,则finalizer正是执行这项任务的合适方法。如果本地对等体占用了关键资源,则类需要定义一个显式终止方法,该方法需要执行释放关键资源的所有必要工作。显式终止方法可以是一个本地方法,或者可以调用一个本地方法。

需要特别注意的是,finalizer链并不会自动执行。如果一个类(非Object)拥有一个finalizer,并且一个子类重写了该方法,则子类的finalizer必须手工调用父类的finalizer。必须在try块中终结子类,在相应的finally块中终结父类。这就保证了即便子类的finalization过程抛出异常,父类的finalizer也会执行。

// Manual finalizer chaining
@Override
protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
如果一个子类实现重写了父类的finalizer,但是忘了手工调用,则父类的finalizer就永远不会执行。要防范这种粗心的或者恶意的子类是可以做到的,代价就是为每个要finalize的对象创建一个额外的对象。不把finalize放在需要终结的类中,而是放到一个匿名类,该匿名类的唯一目的就是终结其外围实例(enclosing instance)。该匿名类的单个实例被称为finalize守护者(finalizer
guardian)
,外围类的每个实例都会创建一个finalize守护者。外围类实例在其私有实例域中保存着一个指向其finalize守护者的唯一引用,这样当外围实例有资格被终结时,其finalize守护者就同时有资格被终结了。当守护者被终结时,它会执行外围类所需要的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是否调用super.finalizer都不要紧。每个拥有finalizer的非final的公有类都需要考虑这种技术。

总之,除非作为安全网,或者为了终止非关键的本地资源,否则都不要使用finalizer。在这些确实需要finalizer的极少情况下,记得要调用super.finalizer。如果你将finalizer用作安全网,记得在finalizer中打印出这种非法使用。最后,如果需要在public、nonfinal的类中使用finalizer,考虑使用finalizer守护者,这样即使子类finalizer没有调用super.finalize,父类的finalizer也会执行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐