1 通常是有延迟的
Finalizer机制是不可预知的,往往是危险的,而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer机制有一些特殊的用途,我们稍后会在这个条目中介绍,但是通常应该避免它们。 从Java 9开始,Finalizer机制已被弃用,但仍被Java类库所使用。 Java 9中 Cleaner机制代替了Finalizer机制。 Cleaner机制不如Finalizer机制那样危险,但仍然是不可预测,运行缓慢并且通常是不必要的。
Finalizer和Cleaner机制的一个缺点是不能保证他们能够及时执行[JLS,12.6]。 在一个对象变得无法访问时,到Finalizer和Cleaner机制开始运行时,这期间的时间是任意长的。 这意味着你永远不应该Finalizer和Cleaner机制做任何时间敏感(time-critical)的事情。 例如,依赖于Finalizer和Cleaner机制来关闭文件是严重的错误,因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行Finalizer和Cleaner机制而导致许多文件被打开,程序可能会失败,因为它不能再打开文件了。
Java规范不能保证Finalizer和Cleaner机制能及时运行;它甚至不能能保证它们是否会运行。当一个程序结束后,一些不可达对象上的Finalizer和Cleaner机制仍然没有运行。因此,不应该依赖于Finalizer和Cleaner机制来更新持久化状态。例如,依赖于Finalizer和Cleaner机制来释放对共享资源(如数据库)的持久锁,会导致分布式系统停滞。
2. 未捕获的异常会被忽略
finalizer机制的另一个问题是在执行Finalizer机制过程中,未捕获的异常会被忽略,并且该对象的Finalizer机制也会终止 [JLS, 12.6]。未捕获的异常会使其他对象陷入一种损坏的状态(corrupt state)。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意不确定的行为。通常情况下,未捕获的异常将终止线程并打印堆栈跟踪( stacktrace),但如果发生在Finalizer机制中,则不会发出警告。Cleaner机制没有这个问题,因为使用Cleaner机制的类库可以控制其线程。
3. 造成严重的性能损失
使用finalizer和cleaner机制会导致严重的性能损失。 在我的机器上,创建一个简单的AutoCloseable
对象,使用try-with-resources关闭它,并让垃圾回收器回收它的时间大约是12纳秒。 使用finalizer机制,而时间增加到550纳秒。 换句话说,使用finalizer机制创建和销毁对象的速度要慢50倍。 这主要是因为finalizer机制会阻碍有效的垃圾收集。 如果使用它们来清理类的所有实例(在我的机器上的每个实例大约是500纳秒),那么cleaner机制的速度与finalizer机制的速度相当,但是如果仅将它们用作安全网( safety net),则cleaner机制要快得多,如下所述。 在这种环境下,创建,清理和销毁一个对象在我的机器上需要大约66纳秒,这意味着如果你不使用安全网的话,需要支付5倍(而不是50倍)的保险。
4. 使用的好处
那么,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
,但不能保证在你的机器上会看到相同的行为。
总之,除了作为一个安全网或者终止非关键的本地资源,不要使用Cleaner机制,或者是在Java 9发布之前的finalizers机制。即使是这样,也要当心不确定性和性能影响。