Effective java 笔记 ---- 第八条 避免使用终结方法和清除方法

探讨了Java中终结方法(finalizer)与清除方法(cleaner)的使用风险,包括不可预测性、性能损失及安全问题。建议使用AutoCloseable接口替代,确保资源正确关闭。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 终结方法(finalizer) 通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性问题。当然,终结方法也有其可用之处,但根据经验,应该避免使用终结方法。在Java9 中用清除方法(cleaner) 代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

2. 终结方法和清除方法的缺点在于不能保证被及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。这意味着,注重时间(time-critical)的任务不应该由终结方法或者清除方法来完成。例如,用终结方法或者清除方法来关闭已经打开的文件,就是一个严重错误,因为打开文件的描述符是一种很有限的资源。如果系统无法及时运行终结方法或清除方法就会导致大量的文件仍然保留在打开状态,于是当一个程序再也不能打开文件的时候,它可能会运行失败。

3. 及时地执行终结方法和清除方法正是垃圾回收算法的一个主要功能。

4. 延迟终结过程并不只是一个理论问题。在很少见的情况下,为类提供终结方法,可能会随意地延迟其实例的回收过程。终结方法线程的优先级比应用程序的其他线程的优先级要低很多。Java语言规范并不保证哪个线程将会执行终结方法,所以除了不使用终结方法以外,并没有很轻便的方法能够避免这样的问题。在这方面,清除方法比终结方法稍好一些,因为类的设计者可以控制自己的清除线程,但清除方法仍然在后台运行,处于垃圾回收器的控制之下,因此不能确保及时清除。

(清除机制可参考:https://blog.csdn.net/weixin_42447959/article/details/81878868 && https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Cleaner.html)

5. Java语言规范不仅不保证终结方法或者清除方法会被及时的执行,而且根本就不保证它们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。结论是:永远不应该依赖终结方法或者清除方法来更新重要的持久状态。例如,依赖终结方法或者清除方法来释放共享资源(比如数据库)上的永久锁,这很容易让整个分布式系统垮掉。

6. 不要被 System.gc 和 System.runFinalization 这两个方法所诱惑,它们确实增加了终结方法或者清除方法的执行机会,但是它们并不保证终结方法或者清除方法一定会被执行。

7. 使用终结方法的另一个问题是:如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止[JLS,12.6]。未被捕获的异常会使对象处于破坏的状态(corrupt state),如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未被捕获的异常将会使线程终止,并打印出栈轨迹(stack trace),但是如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来(finalizer攻击参考:https://blog.csdn.net/sumoyu/article/details/23909905 &&  https://www.ibm.com/developerworks/cn/java/j-fv/index.html)。清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程。

8. 使用终结方法和清除方法有一个非常严重的性能损失。在我的机器上,创建一个简单的 AutoCloseable对象,用try-with-resource将它关闭,再让垃圾回收器将他回收,完成这些工作花费的时间大约为12ns。增加一个终结方法使时间增加到了550ns。换句话说,用终结方法创建和销毁对象慢了大约50倍。这主要是因为终结方法阻止了有效的垃圾回收。如果用清除方法来清除类的所有实例,它的速度比终结方法会稍微快一些(在我的机器上大约是每个实例花500ns),但如果只是把清除方法作为一道安全网,那么清除方法的速度还会更快一些。在这种情况下,创建、清除和销毁对象,在我的机器上花了大约66ns,这意味着,如果没有使用它,为了确保安全网多花了5倍(而不是50倍)的代价。

9. 终结方法有一个严重的安全问题:它们为终结方法攻击(finalizer attack)打开了类的大门。终结方法攻击背后的思想很简单:如果从构造器或者它的序列化对等体(readObject 和 readResolve方法)抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法。从构造器抛出的异常应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。这种攻击可能造成致命的后果。final类不会受到终结方法攻击,因为没人能够编写出final类的恶意子类。为了防止非 final 类受到终结方法攻击,要编写一个空的final的finalize方法。

10. 如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么才能不用编写终结方法或者清除方法呢?只需让类实现AutoCloseable,并要求其客户端在每个实例不再需要的时候调用close方法,一般是利用try-with-resources确保终止,即使遇到异常也是如此。值得提及的一个细节是,该实例必须记录下自己是否已经被关闭了:close方法必须在一个私有域中记录下“该对象已经不再有效”。如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常。(https://www.jianshu.com/p/3a1e774d8625关于AutoCloseable)

11. 终结方法和清除方法有什么好处呢?它们有两种合法用途。第一种用途是,当资源的所有者忘记调用它的close方法时,终结方法或清除方法可以充当“安全网”。虽然这样做并不能保证终结方法或清除方法会被及时地运行,但是在客户端无法正常结束操作的情况下,迟一点释放资源总比永远不释放要好。如果考虑编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价。有些java类(如FileInputStream、FileOutputStream、ThreadPoolExecutor 和 java.sql.Connection) 都具有能充当安全网的终结方法。

清除方法的第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地(非java的)对象(native object), 普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。如果本地对等体没有关键资源,并且性能也可以接受的话,那么清除方法或者终结方法正式执行这项任务最合适的工具。如果本地对等体拥有必须被及时终止的资源,或者性能无法接受,那么该类就应该具有一个close方法。

12. 以一个简单的Room类为例来认识一下清除方法。假设房间在收回之前必须进行清除。Room类实现了AutoCloseable;它利用清除方法自动清除安全网的过程只不过是一个实现细节。与终结方法不同的是,清除方法不会污染类的公有API:

内嵌的静态类State保存清除方法清除房间所需的资源。在这个例子中,就是numJunkPiles域,表示房间的杂乱度。更现实地说,它可以是final的long,包含一个指向本地对等体的指针。State实现了Runnable接口,它的run方法最多被Cleanable调用一次,后者是我们在Room构造器中用清除器注册State实例时获得的。以下两种情况之一会触发run方法的调用:通常是通过调用Room的close方法触发的,后者又调用了Cleanable的清除方法。如果到了Room实例应该被垃圾回收时,客户端还没有调用cloase方法,清除方法就会(希望如此)调用State的run方法。

关键是State实例没有引用它的Room实例。如果它引用了,会造成循环,阻止Room实例被垃圾回收(以及防止被自动清除)。因此State必须是一个静态的嵌套类,因为非静态的嵌套类包含了对其外围实例的引用。同样地,不建议使用lambda,因为他们很容易捕捉到对外围对象的引用。

如前所述,Room的清除方法只用作安全网。如果客户端将所有的Room实例化都包在try-with-resource块中,将永远不会请求到自动清除。用下面这个表现良好的客户端代码示范一下:

正如所期待的一样,运行Adult程序会打印出Goodbye,接着是 Cleaning room。

上面这段代码,你可能期望打印出 Peace out,然后是 Cleaning room,但是在我的机器上,没有打印出Cleaning room,就推出程序了。这就是之前提到过的不可预见性。

Cleaner规范指出:“清除方法在System.exit期间的行为是与实现相关的。不确保清除动作是否会被调用。” 虽然规范没有指明,其实对于正常的程序退出也是如此。在我的机器上,只要在Teenager的main方法上添加代码行 System.gc(),就足以让它在退出之前打印出Cleaning room,但是不能保证在你的机器上也能看到相同的行为。

13. 总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于Java 9之前的发行版本,则尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性和性能后果。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值