您的位置:首页 > 编程语言 > Java开发

7. 【创建和销毁对象】避免使用终结方法finalize

2016-05-03 23:19 417 查看
本文是《Effective Java》读书笔记第7条,其中内容可能会结合实际应用情况或参考其他资料进行补充或调整。

终结方法(finalize)通常是不可预测的,也是很危险的,因此一般情况下应该尽量避免使用终结方法。

对于C++程序猿来说,析构方法是再熟悉不过了,它常用来回收对象所占用资源,以及回收其他非内存资源等。但是我们通常会告诫C++转到Java的程序猿,“Java中的终结方法与析构方法是相当不同的”。

在Java中,回收对象所占用资源的事情交给垃圾回收器来执行了,而手动回收对象外资源的事情也通常交由try - finally来完成。

吐槽终结方法

并不保证及时性

首先要吐槽的一点就是,终结方法并不能保证被及时执行。从一个对象变为不可达,到它的终结方法被执行,中间所花费的时间是任意长的。所以,注重时效性的任务不应交由“不靠谱”的终结方法来完成。比如,使用完文件后将其关闭的动作如果放在终结方法中,那么一旦JVM延迟执行终结方法,那么会有大量的文件保留在打开状态,而文件是一种很有限的资源,从而会影响到程序的性能甚至准确性和健壮性。

通常终结方法由垃圾回收算法来执行,而这种算法在不同的JVM实现中会大相径庭。所以依赖终结方法的话,很可能会出现测试效果与实际应用效果不同的情况。在垃圾回收过程中的终结方法线程的优先级相对来说要低得多,所以交给它来执行的事情通常如同交代一件事给一个严重拖延症患者。

并不保证会执行

有时候当一个程序终止的时候,某些已经无法访问的对象上的终结方法根本没有被执行,这是完全有可能的。

System.gc
System.runFinalization
这两个方法可以增加终结方法被执行的机会,但是不保证一定被执行。而唯一声称保证终结方法被执行的两个方法是
System.runFinalizersOnExit
Runtime.runFinalizersOnExit
,但由于两个方法都有致命缺陷,已经被废弃了。。。

看到这,估计你会怀疑当初的Java语言设计者为啥要放这么一个形同阑尾的鸡肋特性。这不仅是个重度拖延症患者,而且执行力也很成问题,如果你是公司老板,肯定不会用这样的员工吧。

继续吐槽

关于终结方法的劣迹其实还有可以扒出来一些。比如假设这样一个情形,如果终结方法中抛出未被捕获的异常,那么这种异常可以被忽略,并且该对象的终结过程都完不成了,就好像这个对象突然穿越到某个次元,处于破坏的状态,其他线程使用这种被破坏的对象的时候就不知道会发生什么奇怪的事情了。更糟糕的是,这种异常压根不会像正常的异常(怎么感觉措辞有点怪怪的。。。)将线程终止并打印出Stack Trace,甚至连警告都看不到。

而且,终结方法由一个非常严重的性能损失,用终结方法创建和销毁对象会慢几十倍几百倍。。。

那如何正确地终止呢?

显式的终止方法

如果类中对象封装的资源确实需要终止,只需要提供一个显示的终止方法,并要求该类的使用方在每个实例不可用的时候调用这个方法即可。

这种例子其实比比皆是,比如
InputStream
OutputStream
java.sql.Connection
等类的
close
方法。

结合try - finally

显式的终止方法通常要结合try - finally结构来使用,以确保及时终止。在finally子句中调用显式的终止方法,可以保证即使在try块中使用该对象的时候抛出异常,然后能够正确执行终止方法。

当然,Java7之后,try with resources特性的加入使得这一过程更加容易和靠谱了。

如何正确使用终结方法呢?

声讨了这么多,终结方法有木有合法的使用途径呢?

“安全网”

这一条完全是本着“有比没有强”的原则来的,迟一点释放关键资源总比永远不释放要好吧,假设对象的使用者忘了调用显示的终止方法,那么这层安全网多少能够保证资源有较大概率是可以被释放的。而且如果终结方法发现资源没有被终止,则应该在日志中记录一条警告,表示由一个Bug,应该得到修复。

FileInputStream
FileOutputStream
Timer
Connection
等类中,都有终结方法,充当安全网的作用。

终止native资源

终结方法的第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象(native object),普通对象(java object)通过本地方法(native method)委托给一个本地对象,因为本地对等体不是一个普通对象,所以垃圾回收器不知道它,当它的java对等体(也就是普通对象)被回收的时候,它不会被回收。

在本地对等体并不拥有关键资源的前提下,终结方法正是执行这项任务合适的工具。如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法,如前所述。

值得一提的重要一点是,“终结方法链”并不会自动执行。如果某个类有终结方法,并且子类覆盖了终结方法,子类的终结方法必须手工调用超类的终结方法。你应该在try块中终结子类,并在finally中调用超类的终结方法,这样能够保证即使子类的终结过程抛出异常,超类的终结方法也能够执行。如下所示:

//手动的终结方法链
@Override protected void finalize() throws Throwable {
try {
... // 终结子类
} finally {
super.finalize();
}
}


如果子类覆盖了超类的终结方法,但是忘了手工调用超类的终结方法,那么超类的终结方法将永远不会被调用到。要防范这样粗心大意或者恶意的子类是有可能的,代价就是为每个将被终结的对象创建一个附加的对象。将终结方法放到一个匿名的类中,该匿名类的唯一用途就是终结它的外围实例。该匿名类的单个实例被称为终结方法守卫者(finalizer guardian),外围类的每个实例都会创建这样一个守卫者。外围实例在它的私有属性中保存着一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程,也就是当守卫者被终结的时候,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样。

// 共有类
public class Foo {
...
// 终结守卫者
private final Object finalizerGuardian = new Object() {
@Override protected void finalize() throws Throwable {
... // 终结外围Foo对象
}
};
...
}


注意到,共有类Foo并没有终结方法(除了从Object中继承的一个无关紧要的外),所以子类的终结方法是否调用super.finalize并不重要。

总结

总之,除了以上两种用法之外,请不要使用终结方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java