Effective Java 第三版——8. 避免使用Finalizer和Cleaner机制

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。java

Effective Java, Third Edition

8. 避免使用Finalizer和Cleaner机制

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

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

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

及时执行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]。ide

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机制有一个严重的安全问题:它们会打开你的类来进行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机制使用起来有点棘手。下面是演示该功能的一个简单的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方法的调用经过如下两种方法触发:一般,经过调用Roomclose方法内调用Cleanableclean方法来触发。 若是在Room实例有资格进行垃圾回收的时候客户端没有调用close方法,那么Cleaner机制将(但愿)调用Staterun方法。

一个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机制。即便是这样,也要小心不肯定性和性能影响。

相关文章
相关标签/搜索