effective-Java/ch02创建和销毁对象/08.避免使用Finalizer和Cleaner机制.md

8.4 KiB
Raw Blame History

避免使用Finalizer和Cleaner机制

Finalizer机制是不可预知的往往是危险的而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer机制有一些特殊的用途我们稍后会在这个条目中介绍但是通常应该避免它们。 从Java 9开始Finalizer机制已被弃用但仍被Java类库所使用。 Java 9中 Cleaner机制代替了Finalizer机制。 Cleaner机制不如Finalizer机制那样危险但仍然是不可预测运行缓慢并且通常是不必要的。

Finalizer和Cleaner机制的一个缺点是不能保证他们能够及时执行。 在一个对象变得无法访问时到Finalizer和Cleaner机制开始运行时这期间的时间是任意长的。 这意味着你永远不应该Finalizer和Cleaner机制做任何时间敏感time-critical的事情。 例如依赖于Finalizer和Cleaner机制来关闭文件是严重的错误因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行Finalizer和Cleaner机制而导致许多文件被打开程序可能会失败因为它不能再打开文件了。

及时执行Finalizer和 Cleaner机制是垃圾收集算法的一个功能这种算法在不同的实现中有很大的不同。程序的行为依赖于Finalizer和 Cleaner机制的及时执行其行为也可能大不不同。 这样的程序完全可以在你测试的JVM上完美运行然而在你最重要的客户的机器上可能运行就会失败。

延迟终结finalization不只是一个理论问题。为一个类提供一个Finalizer机制可以任意拖延它的实例的回收。一位同事调试了一个长时间运行的GUI应用程序这个应用程序正在被一个OutOfMemoryError错误神秘地死掉。分析显示在它死亡的时候应用程序的Finalizer机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是Finalizer机制线程的运行优先级低于其他应用程序线程所以对象被回收的速度低于进入队列的速度。语言规范并不保证哪个线程执行Finalizer机制因此除了避免使用Finalizer机制之外没有轻便的方法来防止这类问题。在这方面 Cleaner机制比Finalizer机制要好一些因为Java类的创建者可以控制自己cleaner机制的线程但cleaner机制仍然在后台运行在垃圾回收器的控制下运行但不能保证及时清理。

Java规范不能保证Finalizer和Cleaner机制能及时运行它甚至不能能保证它们是否会运行。当一个程序结束后一些不可达对象上的Finalizer和Cleaner机制仍然没有运行。因此不应该依赖于Finalizer和Cleaner机制来更新持久化状态。例如依赖于Finalizer和Cleaner机制来释放对共享资源(如数据库)的持久锁,这是一个使整个分布式系统陷入停滞的好方法。

不要相信System.gcSystem.runFinalization方法。 他们可能会增加Finalizer和Cleaner机制被执行的几率但不能保证一定会执行。 曾经声称做出这种保证的两个方法:System.runFinalizersOnExit和它的孪生兄弟Runtime.runFinalizersOnExit,包含致命的缺陷,并已被弃用了几十年[ThreadStop]。

Finalizer机制的另一个问题是在执行Finalizer机制过程中未捕获的异常会被忽略并且该对象的Finalizer机制也会终止。未捕获的异常会使其他对象陷入一种损坏的状态corrupt state。如果另一个线程试图使用这样一个损坏的对象可能会导致任意不确定的行为。通常情况下未捕获的异常将终止线程并打印堆栈跟踪 stacktrace但如果发生在Finalizer机制中则不会发出警告。Cleaner机制没有这个问题因为使用Cleaner机制的类库可以控制其线程。

使用finalizer和cleaner机制会导致严重的性能损失。 在我的机器上,创建一个简单的AutoCloseable对象使用try-with-resources关闭它并让垃圾回收器回收它的时间大约是12纳秒。 使用finalizer机制而时间增加到550纳秒。 换句话说使用finalizer机制创建和销毁对象的速度要慢50倍。 这主要是因为finalizer机制会阻碍有效的垃圾收集。 如果使用它们来清理类的所有实例(在我的机器上的每个实例大约是500纳秒)那么cleaner机制的速度与finalizer机制的速度相当但是如果仅将它们用作安全网 safety net则cleaner机制要快得多如下所述。 在这种环境下创建清理和销毁一个对象在我的机器上需要大约66纳秒这意味着如果你不使用安全网的话需要支付5倍(而不是50倍)的保险。

finalizer机制有一个严重的安全问题它们会打开你的类来进行finalizer机制攻击。finalizer机制攻击的想法很简单如果一个异常是从构造方法或它的序列化中抛出的——readObject和readResolve方法(第12章)——恶意子类的finalizer机制可以运行在本应该“中途夭折died on the vine”的部分构造对象上。finalizer机制可以在静态字属性记录对对象的引用防止其被垃圾收集。一旦记录了有缺陷的对象就可以简单地调用该对象上的任意方法而这些方法本来就不应该允许存在。从构造方法中抛出异常应该足以防止对象出现而在finalizer机制存在下则不是。这样的攻击会带来可怕的后果。Final类不受finalizer机制攻击的影响因为没有人可以编写一个final类的恶意子类。为了保护非final类不受finalizer机制攻击编写一个final的finalize方法,它什么都不做。

那么,你应该怎样做呢?为对象封装需要结束的资源(如文件或线程)而不是为该类编写Finalizer和Cleaner机制让你的类实现AutoCloseable接口即可并要求客户在在不再需要时调用每个实例close方法通常使用try-with-resources确保终止即使面对有异常抛出情况条目 9。一个值得一提的细节是实例必须跟踪是否已经关闭close方法必须记录在对象里不再有效的属性其他方法必须检查该属性如果在对象关闭后调用它们则抛出IllegalStateException异常。

那么Finalizer和Cleaner机制有什么好处呢它们可能有两个合法用途。一个是作为一个安全网safety net以防资源的拥有者忽略了它的close方法。虽然不能保证Finalizer和Cleaner机制会迅速运行(或者根本就没有运行)最好是把资源释放晚点出来也要好过客户端没有这样做。如果你正在考虑编写这样的安全网Finalizer机制请仔细考虑一下这样保护是否值得付出对应的代价。一些Java库类FileInputStreamFileOutputStreamThreadPoolExecutorjava.sql.Connection都有作为安全网的Finalizer机制。

第二种合理使用Cleaner机制的方法与本地对等类native peers有关。本地对等类是一个由普通对象委托的本地(非Java)对象。由于本地对等类不是普通的 Java对象所以垃圾收集器并不知道它当它的Java对等对象被回收时本地对等类也不会回收。假设性能是可以接受的并且本地对等类没有关键的资源那么Finalizer和Cleaner机制可能是这项任务的合适的工具。但如果性能是不可接受的或者本地对等类持有必须迅速回收的资源那么类应该有一个close方法,正如前面所述。

Cleaner机制示例

示例代码Item08Example01.java:假设Room对象必须在被回收前清理干净。

Room类实现AutoCloseable接口它的自动清理安全网使用的是一个Cleaner机制这仅仅是一个实现细节。

Cleaner机制的规范说System.exit方法期间的清理行为是特定于实现的。 不保证清理行为是否被调用。”虽然规范没有说明,但对于正常的程序退出也是如此。 在我的机器上,将System.gc()方法添加到Teenager类的main方法足以让程序退出之前打印Cleaning room,但不能保证在你的机器上会看到相同的行为。

总之除了作为一个安全网或者终止非关键的本地资源不要使用Cleaner机制或者是在Java 9发布之前的finalizers机制。即使是这样也要当心不确定性和性能影响。