【第8条】避免使用终结方法和清除方法

避免使用终结方法和清除方法

 

最终方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer 机制有一些特殊的用途,我们稍后会在这个条目中介绍,但是通常应该避免它们。从 Java 9 开始,Finalizer 机制已被弃用,但仍被 Java 类库所使用。 Java 9 中Cleaner 机制代替了 Finalizer 机制。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的

 

提醒 C++程序员不要把 Java 中的 Finalizer 或 Cleaner 机制当成的 C++ 析构函数的等价物。在C++ 中,析构函数是回收对象相关资源的正常方式,是与构造方法相对应的。在 Java 中,当一个对象变得不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外的工作。 C++ 析构函数也被用来回收其他非内存资源。在 Java 中,try-with-resources 或 try-finally 块用于此目的(详见第 9 条)。

 

Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行[JLS,12.6]。在一个对象变得无法访问时,到 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语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。结论是:永远不应该依赖终结方法或者清除方法来更新重要的持久状态。例如,依赖终结方法或者清除方法来释放共享资源(比如数据库)上的永久这很容易让整个分布式系统垮掉。

 

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

 

Finalizer 机制的另一个问题是在执行 Finalizer 机制过程中,未捕获的异常会被忽略,并且该对象的Finalizer 机制也会终止 [JLS, 12.6]。未捕获的异常会使其他对象陷入一种损坏的状态(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 attack)打开了类的大门。终结方法攻击背后的思想很简单:如果从构造器或者它的序列化对等体( readObject 和 readResolve方法,详见第12章)抛出异常,恶意子类的终结方法就可以在构造了部分的应该已经半途天折的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法。从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。这种攻击可能造成致命的后果。 final类不会受到终结方法攻击,因为没有人能够编写出 final类的恶意子类。为了防止非fnal类受到终结方法攻击,要编写一个空的final的finalize方法。

 

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

 

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

 

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

 

Cleaner 机制使用起来有点棘手。下面是演示该功能的一个简单的 Room 类。假设 Room 对象必须在被回收前清理干净。Room 类实现 AutoCloseable 接口;它的自动清理安全网使用的是一个Cleaner 机制,这仅仅是一个实现细节。与 Finalizer 机制不同,Cleaner 机制不污染一个类的公共 API:

// An autocloseable class using a cleaner as a safety net public class Room implements AutoCloseable {   private static final Cleaner cleaner = Cleaner.create();   // Resource that requires cleaning. Must not refer to Room!   private static class State implements Runnable {     int numJunkPiles; // Number of junk piles in this room         State(int numJunkPiles) {       this.numJunkPiles = numJunkPiles;     }    // Invoked by close method or cleaner     @Override public void run() {       System.out.println("Cleaning room");       numJunkPiles = 0;     }   }  // The state of this room, shared with our cleanable   private final State state;     // Our cleanable. Cleans the room when it’s eligible for gc   private final Cleaner.Cleanable cleanable;    public Room(int numJunkPiles) {     state = new State(numJunkPiles);     cleanable = cleaner.register(this, state);   }  @Override public void close() {     cleanable.clean();   } }

静态内部 State 类拥有 Cleaner 机制清理房间所需的资源。在这里,它仅仅包含 numJunkPiles属性,它代表混乱房间的数量。更实际地说,它可能是一个 final 修饰的 long 类型的指向本地对等类的指针。State 类实现了 Runnable 接口,其 run 方法最多只能调用一次,只能被我们在 Room构造方法中用 Cleaner 机制注册 State 实例时得到的 Cleanable 调用。对 run 方法的调用通过以下两种方法触发:通常,通过调用 Room 的 close 方法内调用 Cleanable 的 clean 方法来触发。如果在 Room 实例有资格进行垃圾回收的时候客户端没有调用 close 方法,那么 Cleaner 机制将(希望)调用 State 的 run 方法。

 

一个 State 实例不引用它的 Room 实例是非常重要的。如果它引用了,则创建了一个循环,阻止了 Room 实例成为垃圾收集的资格(以及自动清除)。因此, State 必须是静态的嵌内部类,因为非静态内部类包含对其宿主类的实例的引用(详见第 24 条)。同样,使用 lambda 表达式也是不明智的,因为它们很容易获取对宿主类对象的引用。

 

就像我们之前说的, Room 的 Cleaner 机制仅仅被用作一个安全网。如果客户将所有 Room 的实例放在 try-with-resource 块中,则永远不需要自动清理。行为良好的客户端如下所示:

public class Adult {   public static void main(String[] args) {     try (Room myRoom = new Room(7)) {       System.out.println("Goodbye");     }   }}

正如你所预料的,运行 Adult 程序会打印 Goodbye 字符串,随后打印 Cleaning room 字符串。但是如果时不合规矩的程序,它从来不清理它的房间会是什么样的? 

public class Teenager {   public static void main(String[] args) {     new Room(99);     System.out.println("Peace out");   } }

你可能期望它打印出 Peace out ,然后打印 Cleaning room 字符串,但在我的机器上,它从不打印 Cleaning room 字符串;仅仅是程序退出了。这是我们之前谈到的不可预见性。 Cleaner 机制的规范说:“ System.exit 方法期间的清理行为是特定于实现的。不保证清理行为是否被调用。”虽然规范没有说明,但对于正常的程序退出也是如此。在我的机器上,将 System.gc() 方法添加到Teenager 类的 main 方法足以让程序退出之前打印 Cleaning room ,但不能保证在你的机器上会看到相同的行为。

 

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

 

                                                                                             关注公众号

                                                                                            每天干货分享

                                                          

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值