写点什么

Java 程序经验小结:避免使用终结方法

发布于: 2021 年 01 月 22 日
Java 程序经验小结:避免使用终结方法

1、写在开头

终结方法(finalizer)通常是不可预测的,也是危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能,以及可移植性问题。但终结方法也有其可用的地方,详见下文。

2、终结方法的弊端

在 java 中,终结方法一般会结合 try-finally 块来一起使用,在 finally 子句内部调用终结方法。


不要被 System.gc 和 System.runFinalization 这两个方法所诱惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定被执行。唯一声称保证终结方法被执行的方法是 System.runFinalizersOnExit,已经它臭名昭著的孪生兄弟 Runtime.runFinalizersOnExit。这两个方法都有致命缺陷,已经被废弃了。


另一个概念是终止方法,典型显式的终止方法例子是:InputStream、OutputStream 和 java.sql.Connection 的 close 方法,还有 java.util.Timer 的 cancel 方法。另外,java.awt 包括了 Graphics.dispose 和 Window.dispose,以及 Image.flush,这些方法由于性能不好而不为关注。


终结方法缺点一:终结方法在于不能保证会被及时的执行。从一个对象变得不可达开始,到它的终结方法被执行,所花费时间是任意长的。这意味着,注重时间(time-critical)的任务不应该由终结方法来完成。

例如,使用终结方法来关闭已经打开的文件,这是严重错误的,因为打开文件的描述符是一种很有限的资源。由于 JVM 会延迟执行终结方法,所以大量的文件会保留在打开状态,当一个程序不能打开文件的时候回导致运行失败。


  1. 垃圾回收算法的主要功能是:及时的执行终结方法,而这种算法在不同的 JVM 有不同的实现。

  2. 终结方法线程的优先级比其他应用程序的的其他线程的要低得多。Java 语言规范并不保证哪个线程将会执行终结方法,甚至根本不能保证它们会被执行。所以,除了不使用终结方法之外,并没有很轻便的步伐能够避免这样的问题。

  3. 不应该依赖终结方法来更新重要的持久状态。例如,终结方法来释放共享资源(数据库)的共享锁,很容易让整个分布式系统垮掉。


终结方法缺点二:如果未被捕获的异常在终结过程中被抛出,那么这种异常可以被忽略,并且该对象的终结过程也会被终结。未被捕获的异常会使对象处于破坏的状态(a corrupt state),如果另一个线程企图使用该对象,则可能发生任何不确定的行为。

正常情况未捕获的异常会使线程终止并打印堆栈轨迹,但如果异常发生在终结方法中,甚至不会打印警告!!


终结方法缺点三:使用了终结方法,会导致严重的性能损失。例如,在某个机器上创建并销毁一个简单对象时间约为 5.6ns,增加一个终结方法则会增加到 2400ns。


3、终结方法的好处-1

终结方法第一种合法用途是:当对象所有者忘记调用前面建议的显式终止方法时,终结方法可以充当“安全网”(safety net)。

虽然这样做不能保证终结方法会被及时执行,但在客户端无法通过显式调用终止方法来正常结束操作的情况下,迟一点释放关键资源总永不释放要好(如果终结方法发现资源仍未被终止,应该在日志中记录一条警告)。

显式终止方法的实例(四个类:FileInputStream、FileOutputStream 、Connection 和 Timer)都具有终结方法,当终止方法不起作用,这些终结方法便当了安全网

4、终结方法的好处-2

终结方法的第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象(native object),普通对象通过本地方法委托给一个本地对象,因为本地对等体不是一个普通对象,所以垃圾回收期并不知道它。因此,在本地对等体并不拥有关键资源时,终结方法正是执行这项任务的最合适工具。


  • 如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法。,这个终止方法就是完成必要的工作并释放关键资源。终止方法可以是本地方法或者它调用本地方法。


  • “终结方法链”(finalizer)同样不会被自动执行,如果类有定义终结方法,并且子类覆盖了该终结方法,那么子类的终结方法就得手工调用父类的终结方法:以确保即使子类的终结方法过程抛出异常,父类的终结方法也会得以执行(这也是规避常见的代码攻击)。


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


  • 另一种方式是使用终结方法守卫者(finalizer guardian)。我们不将终结方法封装在一个要求终结处理的类中,二是放在一个匿名类里,该匿名类唯一用途是终结它的外围实例(enclosing instance),该匿名类的单个实例就被称为终结方法守卫者。外部实例在它的私有实例域中保存着一个对其终结方法守卫者的唯一引用,因此两者可以同时启动和终结过程。



// Finalizer Guardian idiompublic 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 并没有终结方法(除了从 Object 继承的 finalize()之外),所以子类的终结方法是否调用 super.finalize 并不重要。对于每个带有终结方法的非 final 公有类,都应该考虑这个方法。

5、总结

总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。

  1. 在很少见的情况下,既然使用了终结方法,就要记住使用 super.finalize。

  2. 如果用作安全网,要记得记录终结方法的非法用法。

  3. 如果需要将终结方法与公有的非 final 类关联起来,请考虑使用终结方法守卫者以确保:即使子类的终结方法没有调用 super.finalize,该终结方法也会被执行。

6、延伸阅读

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式


《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭


《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》


《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

《读懂排序算法(六):二分查找算法》


《设计模式》

设计模式之六大设计原则

设计模式之创建型(1):单例模式

设计模式之创建型(2):工厂方法模式

设计模式之创建型(3):原型模式

设计模式之创建型(4):建造者模式


发布于: 2021 年 01 月 22 日阅读数: 12
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
Java 程序经验小结:避免使用终结方法