ITEM 8: AVOID FINALIZERS AND CLEANERS
finalizer 是不可预测的,通常是危险、不必要的。使用它们的可能导致不可预测的行为、性能差和糟糕的移植性。finalizer 有一些用途,我们将在本条的后面介绍,但是作为一个规则,您应该避免使用它们。finalizer 在Java 9 中已经废弃,但是Java库仍然在使用它们。在Java 9 中,cleaners 替代了 finalizer。cleaners 比 finalizer 更安全,但仍然不可预测,速度很慢,而且通常没有必要。
c++程序员不要将 finalizer 看成是c++的析构函数。在c++中,析构函数是回收与对象相关资源的常用方法,是构造函数的必要对应项。在Java中,当对象变得不可访问时,垃圾收集器会回收与该对象关联的存储,不需要程序员做特别的工作。c++析构函数还用于回收其他非内存资源。在Java中,使用try-with-resources或try-finally块来完成此目标。
finalizer 和 cleaner 的一个缺点是JVM不能保证它们会立即执行。从对象变得不可访问到 finalizer 或 cleaner 运行之间可以间隔任意长的时间。这意味着您永远不应该在 finalizer 或 cleaner 中执行任何时间相关的操作。例如,由于打开的文件描述符是有限的资源,依赖 finalizer 或 cleaner 来关闭文件是一个严重的选择。由于系统在运行finalizer 或 cleaner 时的延迟,导致许多文件仍然保持打开状态,程序可能会失败,因为系统的文件描述符已经耗尽,不能再打开文件。
及时执行 finalizer 和 cleaner 是垃圾收集算法的一个功能,而垃圾收集算法在不同的JVM实现中存在很大差异。如果程序的行为依赖 finalizer 和 cleaner 的执行,那么结果可能在不同JVM实现上出现不同的情况。在程序员的机器上,结果是正确的,但在用户的机器上可能会失败。
延迟终结不仅仅是一个理论问题。为类提供 finalizer 可以任意延迟其实例的回收。一位同事长时间调试一个运行的GUI应用程序,该应用程序可能神秘地出现OutOfMemoryError错误。分析表明,在程序崩溃的时候,应用程序的 finalizer 队列上有成千上万的图形对象,它们正等着最终被终结和回收。不幸的是,finalizer 线程的优先级比另一个应用程序线程低,所以对象的最终回收速度没有达到预期速度。Java语言规范没有保证哪个线程将执行 finalizer,因此除了避免使用 finalizer 之外,没有其他方法来防止这类问题。在这方面,cleaner 比 finalizer 好一点,因为类作者可以控制自己的 cleaner 线程,但是 cleaner 仍然在后台运行,在垃圾收集器的控制下,所以不能保证及时清理。
Java规范不仅不能保证 finalizer 或 cleaner 能够及时运行;甚至并不能保证它们会运行。甚至很有可能在程序终止时,在一些不可达的对象上仍然没有运行它们。因此,永远不要依赖 finalizer 或 cleaner 来更新持久状态。例如,依赖 finalizer 或 cleaner 来释放共享资源(如数据库)上的持久锁,是使整个分布式系统陷入停顿的好方法。
不要被 System.gc 和 System.runFinalization 欺骗,它们也许能增加 finalizer 或 cleaner 被执行的几率,但它们不能保证这一点。曾经有两种方法声称可以保证这一点: System.runFinalizersOnExit 和 Runtime.runFinalizersOnExit, 然而这些方法存在致命缺陷,几十年来一直遭人诟病。
finalizer 的另一个问题是,在终结期间抛出的未捕获异常将被忽略,同时该对象的终结将终止。未捕获的异常会使其他对象处于损坏状态。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意的不确定性行为。通常,未捕获异常将终止线程并打印堆栈跟踪,但如果它发生在 finalizer 中,则不会这样做——它甚至不会打印警告。cleaner 没有这个问题,因为使用 cleaner 的库可以控制它的线程。
使用 finalizer 和 cleaner 会导致严重的性能损失。在我(作者)的机器上,创建一个简单的 AutoCloseable 对象,使用try-with-resources关闭它,并让垃圾收集器回收它的时间大约是12ns。使用 finalizer 将时间增加到了 550 ns。换句话说,使用finalizer销毁对象要慢50倍左右。这主要是因为终结器抑制了有效的垃圾收集。如果您使用cleaner 来清理类的所有实例(我的机器上的每个实例约为 500 ns),那么 cleaner 的速度与 finalizer 相当,但是如果仅将其用作安全网,则清理器的速度要快得多,如下所述。在这种情况下,在我的机器上,创建、清理和销毁一个对象大约需要花费66纳秒,这意味着如果您不使用它,您将为安全网支付5(而不是50)倍的保险。
finalizer 有一个严重的安全问题: 容易被利用进行 finalizer 攻击。终结器攻击背后的思想很简单:如果从构造函数或其序列化等价对象(readObject和readResolve方法,第12章)抛出异常,恶意子类的 finalizer 可以运行在部分构造的对象上,该对象本应“中途夭折”。此 finalizer 可以在静态字段中记录对对象的引用,从而防止对其进行垃圾收集。一旦记录了这个畸形的对象,就很容易调用这个对象上的任意方法,而这些方法本来就不应该被允许存在。从构造函数抛出异常应该足以阻止对象的产生,但在finalizer 存在的情况下,就不是这样了,这样的攻击可能会产生可怕的后果。Final 类不受 finalizer 攻击的影响,因为没有人可以编写 Final 类的恶意子类。要保护非final类不受终结器攻击,请编写一个不做任何操作的 final finalize 方法。
那么,当需要为一个类编写回收资源(文件、线程)的方法,应该用什么替代 finalizer 和 cleaner ?只需让您的类实现 AutoCloseable,并要求它的客户端在不再需要它时对每个实例调用 close 方法,通常使用try-with-resources来确保即使在异常情况下也终止。一个值得一提的细节是,实例必须跟踪是否已经关闭:关闭方法必须记录字段的对象不再是有效的,其他方法必须验证这个字段,如果对象后已经关闭则抛出IllegalStateException 异常。
那么,finalizer 和 cleaner 到底有什么用呢?它们可能有两种合法用途。一是作为一个兜底措施,以防用户忘记调用实例的close方法。虽然不能保证 finalizer 和 cleaner 会立即(或者根本不会)运行,但是如果用户不能及时释放资源,那么迟释放总比不释放要好。如果您正在考虑编写这样一个兜底措施,请仔细考虑这种保护是否值得付出代价。一些Java库类,如FileInputStream、FileOutputStream、ThreadPoolExecutor和 java.sql.Connection,都有作为兜底的 finalizer。
第二个合法使用与本地对等类(native peers)有关。本地对等类是一个由普通Java对象通过本地方法委托的对象。由于本地对等类不是普通Java对象,所以垃圾收集器不知道它,并且不能在回收委托其的Java对象时回收它。在性能可以接受、并且本地对等类没有关键资源的情况下,finalizer 和 cleaner 适合完成此类任务。如果性能不佳,或者本地对等类持有必须立即回收的资源,则应该在委托的Java对象中创建一个close方法,如前所述。
cleaner 的使用有点棘手。下面是一个简单的类 Room ,让我们假设 Room 对象在回收之前必须清理资源。Room类实现接口 AutoCloseable;事实上,它的自动清洁安全网使用 cleaner 只是一个实现细节。与 finalizer 不同,cleaner不会污染类的公共API:
// An auto closeable 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 持有一个清理器资源。在本例中,它只是 字段 numJunkPiles,表示房间混乱的程度。更实际地说,它可能是 final long 类型,存储指向本地对等类的指针。State类 实现了 Runnable 接口,它的run方法最多只调用一次,由Room对象创建时注册state对象时获得的Cleanable调用。对run方法的调用将由以下两种情况之一触发:通常,它由Room的close方法(调用Cleanable的clean方法)触发;如果客户端未能调用close方法,那么 cleaner 将(希望如此)调用State的run方法。
需要注意的是:State实例不能引用它的房间Room实例。如果这样做将产生一个循环依赖,从而导致Room实例不能被垃圾收集(以及自动清理)。因此,State必须是一个静态内部类,因为非静态内部类包含对其外部实例的引用。同样不建议使用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");
}
}
}
正如我们所期望的,运行main() 打印 "Goodbye",然后是"Cleaning room"。但是这个从不调用close()方法的行为是否是不雅的程序呢?
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("Peace out");
}
}
你可能希望它打印"Peace out",然后是"Cleaning room",但在我(作者)的机器上,它从来不打印"Cleaning room",它只是退出。这就是我们之前提到的不可预测性。cleaner规范说:“cleaner 在系统期间的行为是特定于实现的。不保证一定调用清理操作”。虽然规范没有这么说,但对于普通程序退出也是如此。在我(作者)的机器上,将System.gc()添加到 Teenager 的 main 方法中,就能在其退出之前打印出"Cleaning room",但不能保证您将在自己机器上看到相同的行为。
总之,不要使用cleaner,或者在Java 9之前的版本中使用finalizer,除非作为安全网或者终止非关键的本地资源。即使这样,也要注意不确定性和性能问题。
网友评论