Effective Java(3rd)-Item8:避免使用finalizers和cleaners

  Finalizers是不可预测的,通常是危险的,一般来说是没有必要的。它们的使用可能会造成不稳定的行为,不佳的性能和可移植性问题。Finalizers有一些有效的用途,在本条目将会介绍,但是作为一个规则,你应该避免使用它们。在Java9开始,finalizers被弃用了,但是在Java库中仍然被使用。在Java9中,finalizers的替代者是cleaners。Cleaners比finalizers的危险性要小,但仍然不可预测,缓慢且通常没有必要。
  提醒C++程序员不要将Java的finalizers和cleaners与析构函数作类比。在C++中,析构函数是回收与对象关联的资源的常用方法,是构造函数必要的对应面。在Java中,垃圾收集器当它变得无法访问时回收与对象关联的存储,不需要程序员作特别处理。C++析构函数也用于回收其他非内存资源。在Java中,try-with-resources 或 try-finally块是用于此目的(item9)。
  finalizers和cleaners的一个缺点是无法保证他们会被及时处理。对象变得不可访问和finalizer或cleaner运行之间可能需要任意长的时间。这意味着你不应该在finalizer或cleaner中做任何时间关键的事情。比如,依赖finalizer或cleaner来关闭文件是一个严重错误,因为打开文件描述符是有限的资源。如果许多文件由于系统在运行finalizers和cleaners时的延迟处于打开状态,程序可能会因为不再能打开文件而失败。
  finalizers和cleaners的执行速度首要取决于垃圾回收算法,不同的垃圾回收算法在不同实现中变化很大。取决于finalizer和cleaner的执行速度的程序的行为同样可能变化。
所以完全有可能你的程序在你测试的JVM上完美运行,但是在你最重要的客户上非常不幸地运行失败。
  缓慢的finalization不只是一个理论问题。为一个类提供finalizer可以任意延迟其实例的回收。一位同事调试一个长时间运行的GUI应用程序,该程序因为OutOfMemoryError神秘地死亡。分析显示,在它死亡时。应用程序在finalizer队列上有数以千计的图形对象,等待被销毁和回收。不幸的是,finalizer线程的运行优先级低于其他应用程序线程,因此对象没有及时被销毁。语言规范不保证哪个线程将执行finalizers,因此没有可移植的方法来阻止这些问题,除了避免使用finalizers。Cleaners在这一方面要比finalizers好一些,因为类作者可以控制他们自己的cleaner线程,但是cleaner仍然在后台运行,在垃圾回收器的控制之下,所以不能保证及时清理。
  该规范不仅不能保证finalizers或cleaners可以及时运行;而且它一点也不保证它们会运行。这完全有可能。甚至可能在程序终止时,某些对象不再可访问时,还没有运行。因此,你应该绝不依赖finalizer或cleaner来更新持久状态。例如,依赖于finalizer或cleaner来释放一个在共享资源上(如数据库)的持久锁是将你整个分布式系统停止运行的好方法。
  不要被System.gc和System.runFinalization方法迷惑。它们可能会增加finalizers和cleaners被执行的记录,但是它们并不能保证。有两种方法曾经声明提供这个保证:System.runFinalizersOnExit 和它邪恶的双胞胎,Runtime.runFinalizersOnExit。这些方法有着致命缺陷并且被弃用数十年了。
  finalizers的另一个问题是在finalization期间被忽略时会抛出未捕获的异常,终止对象的终结(finalization)。未捕获的异常会导致其他对象处于损坏的状态。如果另一个线程尝试使用这样损坏的对象,可能会造成任意非确定性行为。通常来说,一个未捕获的异常将终止线程并打印堆栈跟踪,但是发生在finalizer中就不会--它甚至不会打印警告。Cleaners没有这个问题因为使用cleaner的库可以控制它的线程。
  使用finalizers和cleaners是会严重影响性能。在我机器上,创建一个简单的AutoCloseable对象,使用using try-with-resources关闭它,并让垃圾回收器回收它花费的时间为12ns。使用finalizer将时间增加到550ns。换句话说,使用finalizers创建和销毁对象的速度要慢50倍。这主要是因为finalizers抑制了高效的垃圾回收。如果使用cleaner来清除所有类的实例(在我的机器上每个实例大概500ns),它的速度和finalizer相当。但是你仅将它们用作安全网,cleaner将会快很多。如下所述,在这些情况下,在我的机器上创建,清理和销毁一个对象花费了66ns,这意味着如果你不使用安全网,你需要支付5倍(而不是50倍)的保险费。
  Finalizers有一个严重的问题:它们打开你的类受到finalizer攻击。finalizer攻击背后的想法很简单:如果一个异常在构造方法或它的序列化等价物-readObject和readResolve方法(12章)被抛出-恶意子类的finalizer可以运行在部分”胎死腹中“的构造的对象上运行。finalizer可以在静态字段中记录对象的引用,防止被垃圾回收。一旦异常的对象被记录了,在这个对象上调用任意方法是一件简单的事情,这些方法本来就不应该被允许存在。从构造方法中抛出异常应该足以防止对象存在;在finalizer面前,并非如此。如此攻击会产生可怕的后果。final类免于finalizer攻击因为没有人可以编写final类的恶意子类。为了保护非final类免受finalizer攻击,请编写一个final finalize方法不执行任何操作。
  除了为一个对象封装需要终止资源的类(例如文件或线程)编写finalizer或cleaner,你应该怎么做呢?只需要将你的类实现AutoCloseable,并要求其客户端在不再需要时调用每个实例上的close方法,通常使用 try-with-resources确保终止,即使在面对异常的情况下 (item9)。一个值得提到的细节是实例必须追踪它是否被关闭:close方法必须在对象不再有效的字段上记录,并且一旦该对象被关闭后调用,其他方法必须检查这个字段并在调用之后抛出IllegalStateException。
  那么,cleaner和finalizer有好的地方吗?它们可能有两个合理的应用。其一是在资源的拥有者忽略了调用它的close方法时,它可以充当安全保障的角色。虽然不能保证cleaner和finalizer会立即运行(或根本不允许),但很迟释放资源总比客户端没有这么做要更好。如果你考虑编写这样类似finalizer的安全保障,请仔细考虑这样的保护的代价是否值得。某些Java类库,比如FileInputStream,FileOutputStream, ThreadPoolExecutor和 java.sql.Connection,就有这样的安全保障的finalizer。
  cleaners的第二个合理用途是与native peers相关。native peer是一个native(非Java)对象,这种对象是由普通对象委托native方法生成。因为一个native peer不是普通对象,所以它对象的Java对象回收时垃圾回收器不认识它也不回收它。cleaner和finalizer可能就是这个任务适合的工具,假设性能是可接受的并且native peer没有保留关键资源。如果性能不可接受或native peer有必须被立即回收的资源,该类应该有close方法,如前所述。
  Cleaner用起来有点棘手。下面是一个简单的房间类展示的用法。我们假设房间在回收前必须被清洁。房间类实现了AutoCloseable;使用cleaner自动清理只是实现的细节。不像finalizer,cleaner不污染类的公共API。

Effective Java(3rd)-Item8:避免使用finalizers和cleaners_第1张图片
image.png

  静态内部类State包含着需要被cleaner清理房间的资源。在这种情况下,它只是numJunkPiles字段,这个字段代表了房间混乱的程度。更实际地说,它可能是一个包含native peer的指针的final字段。state实现了Runnable,它的run方法最多被调用一次,当我们在room构造方法中使用cleaner注册State实例时得到了Cleanable。run方法的调用将会被下列两种方法之一触发:通常它通过room的close方法来调用cleanable的clean方法被触发。如果客户端无法在room实例符合垃圾回收条件下调用close方法,cleaner(最好)会调用state的run方法。
  state实例不引用它的room实例是重要的。如果引用了,这将创建一个循环,将阻止room实例被垃圾回收(以及被自动清理)。所以,state必须是一个静态内部类,因为非静态内部类包含了对其封闭实例的引用(item24),使用lambda同样不可取,因为它们可以轻易捕获封闭对象的引用。
  正如我们之前所述,room的cleaner只用作安全保障。如果客户端在try-with-resource 块中围绕所有room实例,自动清理将再也不需要。这个良好表现的客户端演示了这种行为:

Effective Java(3rd)-Item8:避免使用finalizers和cleaners_第2张图片
image.png

  正如你所期望的,允许Adult程序会打印GoodBye,随后room被清理。但是下列的不良行为的程序,从不clean它的room呢?
Effective Java(3rd)-Item8:避免使用finalizers和cleaners_第3张图片
image.png

  你可能会期望它将打印Peace out,并清理room,但是在我的机器上,它从不打印清理room的信息,它只是退出了。这就是之前谈到的不可预知性。cleaner规范说,“在System.exit期间cleaner的行为是特定的实现,不保证是否调用清理行为。”虽然规范没有明说,但是正常程序退出也是一样的。在我的机器上,在Teenager的main方法中加入System.gc()足以让它在退出之前打印清理room的信息,但是这不保证在你的机器上也有相同行为。
  总结,不要使用cleaner和finalizer,在java9之前的版本也不行,除非作为一个安全保障或终止非关键本地资源。即使是这样,小心不确定性和性能后果。
本文写于2019.1.13,历时11天

你可能感兴趣的:(Effective Java(3rd)-Item8:避免使用finalizers和cleaners)