创建销毁对象(第八条:杜绝使用FINALIZERS和CLEANERS)

Finalizers是不可预期的,通常很危险,并且基本没有必要。使用它们会引起行为怪异,性能变差,还有其他的一些小的问题。Finalizers是有一些有价值的用途的,这些我们后面会说,但是,你应该把避免使用它们作为一条准则。就Java 9而言,finalizers被摒弃了,但是Java类库仍然在使用它。Java9里面用cleaners来代替finalizers。比起finalizers而言,Cleaners没那么危险,但是仍然不可预期,慢,并且基本没有必要

C++程序员会被警告不要将Java里面的finalizers或者cleaners对等成C++里面的析构函数。在C++里面,析构函数作为构造函数的对立面是有必要的,它是回收对象相关资源的常见的方式。在Java里面,当与一个对象相关的存储资源变得不可达,垃圾收集器起就会回收它们,不需要开发人员做任何事情。C++析构函数还被用来回收一些非内存的资源。在Java里面,try -with-resources或者try - finally用来达到这一目的(Item 9)。finalizers和cleaners的一个缺点是没有什么能够保证它们能被及时的执行。一个对象变成不可达的时间和这个对象的finalizer还有cleaner被调用的时间,可能会间隔任意长的时间。这就意味着你不应该在finalizer或者cleaner里面做任何时间要求严格的事情。比如,依赖finalizer或者cleaner来关闭文件就是一个重大的错误,因为打开文件描述符是有限的资源。如果由于系统运行finalizers或者cleaners的延迟导致文件还是保持打开的状态,那么程序可能会因为它不能在打开文件而失败。

finalizers和cleaners执行的及时性主要取决于垃圾回收算法,而它却在不同的实现里面有着很大的变化。所以一个依赖finalizer或者cleaner执行的及时性的程序也会随着变化。这种程序在你测试的一个JVM上跑的贼溜,然后在某个你最重要的客户那儿推崇的JVM挂的一塌糊入。延迟finalization并不只是一个理论上的问题。为一个类提供一个finalizer会使得回收它的对象出现不可预料的延迟。一个同事调试过一个跑了很久的GUI程序,而它很神秘的抛出了OutOfMemoryError而挂掉了。分析表明,在它死掉的时候,程序中有成千上万的图形对象在finalizer的队列里面等待被finalized和回收。不幸的是,相比于其它的应用线程,finalizer线程是跑在低优先级里面的。所以在那些对象可以执行finalization的时候,它们却没有被finalized。在语言说明中没有保证说哪个线程去执行finalizers,所以除了不去使用finalizers,没有什么方便的方法可以防止这种问题。在这一点上Cleaners比finalizers要好一些,因为写这个类的作者对它的cleaner线程是有控制的,但是cleaners仍旧受控于垃圾收集器跑在背后的,所以不能保证及时清理。说明不仅不能保证finalizers和cleaners会及时的去跑;它甚至都不能保证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对象,到使用try -with-resources关闭它,再到垃圾收集器回收它大概12纳秒。而使用finalizer会将时间增加到550纳秒。换句话而言,用finalizers创建和销毁对象大概要慢50倍。这主要因为finalizers印制了有效的垃圾回收。用Cleaners清除类的所有实例跟finalizers是差不多的(在我的机器上每个对象大约500纳秒),但是如果你将它们用作安全保障,就像下面讨论的,cleaners就会快很多。在这种情况下,在我的机器上,创建,清除和摧毁一个对象大约需要66纳秒,这意味着不过你不使用它,你要为了安全保障你要付出五分之一(不是五十五分之一)的担保。

Finalizers有严重的安全问题:面对finalizer攻击的时候它们会将你的类暴露出来。finalizer攻击背后的想法很简单:如果一个异常从构造器中或者同等于构造器的序列化中(readObject和readResolve方法)被抛出来。一个恶意的子类的finalizer就会基于本应该“中途夭折”的构造了一半的对象去运行。这个finalizer可以将这个对象引用记录在静态域里面从而阻止它被垃圾回收。一旦这种畸形的对象被记录下来,调用这个对象上的任意方法就是小事一桩了,但是这个对象首先是不应该允许它存在的。在构造器中抛出一个异常应该足够去阻止对象的形成;然而在finalizers存在的情况下,并不是这样。这种攻击会导致可怕的结果。Final的类对finalizer攻击是免疫的,因为没人能写一个final类的恶意的子类。为了保护非final的类免于这种攻击,写一个什么都不做的final类型的finalize方法

那么当面临需要终止存有资源的对象比如文件或者线程的时候,不写finalizer或者cleaner你该做什么呢?仅仅让你的类实现AutoCloseable,并且在当对象不在需要的时候,要它的客户端在每个实例中调用close方法。尽管在面临异常的情况下,通常使用try -with-resources来保证终止(Item 9)。有一个细节需要注意,这个对象必须对它是否关闭保持追踪:close方法必须将对象无效的对象记录在一个字段里面,如果对象已经被关闭了其他方法又去调用它,那些方法必须检查这个字段并且抛出IllegalStateException。

那么存不存在cleaners和finalizers的有好处的地方。它们可能会有两个合理的使用。一个是在资源的拥有者忽略了它的close方法的调用时,它可以充当安全保障的角色。尽管cleaner和finalizer即使的调用(或者根本不会调用)是没有保证的,那么如果客户端没有释放资源的时候,迟点总比永远都不要好。如果你要写这种用于安全保障的finalizer,多想想甚至要费劲的想想,这种保护付出的代价是否值得。一些java类库,比如FileInputStream,FileOutputStream,ThreadPoolExecutor,还有java.sql.Connection就有这种用于安全保障的finalizers。cleaners的第二个合理的用途是与有native peers的对象相关。native peer是native的(非java的)对象,这种对象是普通对象委托native 方法生成的。因为native peer不是正常的对象,所以当与之对等的Java对象被收集的时候,垃圾收集器不认识它,也不会回收它。cleaner或者finalizer就可能是这种问题的合适的方式,假设性能是可以接受的,并且native peer没有握着重要的资源。如果性能要求严格或者native peer所拥有的资源必须被及时回收,那么就应该像上面一样,给类中提供一个close方法。

Cleaners用起来是有点技巧的。下面是强调这个机制的一个简单的类Room。让我们假设rooms被回收之前必须先clean。Room类实现了AutoCloseable;使用cleaner的自动清除安全保障的这一事实仅仅是一个详细实现。不像finalizers,cleaners不会污染一个类的公共API。

// An autocloseable class using a cleaner as a safety net
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 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();
    }

    // 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;
        }
    }
}


这个静态的嵌套State类持有需要cleaner用来清除room的资源。在这个例子里面,它就是numJunkPiles字段,代表了房间里垃圾堆的数量的。更实际的讲,它可能是final类型的long,这个long包含了native peer的指针。State实现了Runnable,它的run方法至多被调用一次,调用它的Cleanable是我们用用Room构造器里面的cleaner将State的实例注册生成的。Run方法的调用会被下面两个情况下的某一种触发:通常情况下,它是通过调用Room的close方法从而调用Cleanable 的 clean方法去触发的。如果当Room实例处于可垃圾回收态,但是客户端没有调用close方法,cleaner会(希望它会吧)调用State.run方法。

State对象没有引用Room实例是很重要的。如果它引用了,它会创建出来一个回路,这个回路会阻止Room实例能够被垃圾回收(还有自动清除)。因此,State必须是一个静态的嵌套类,因为非静态的嵌套类包含他们外层类的引用(Item 24)。同样的使用lambda表达式是不建议的,因为他们可以轻易的得到外层对象的引用。

就像我们之前提到的,Room的cleaner只是用于安全保障。如果客户端使用try -with-resource blocks将所有Room的实例包起来,那么自动清除就不需要了。下面这个好的例子强调了这一点:

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的过程中cleaners的行为是根据特殊实现而定的。而清除工作会不会调用它不做保证。”尽管文档没这么说,但是正常程序退出都是会清除的。在我的机器上,在Teenager的main方法中添加System.gc()就足以让程序退出前打印Cleaning room,但是不能保证在你的机器上也能看到相同的行为。

总之,不要使用cleaners,或者早于Java 9不要使用cleaners,还有finalizers,除非使用它们提供安全保障或者终止不重要的native资源。尽管是那样,也要注意不可确定性还有导致的性能结果。
阅读更多
个人分类: Effective Java 3
上一篇创建销毁对象(第七条:消除掉废弃的对象引用)
下一篇创建销毁对象(第九条:比起TRY – FINALLY要更喜欢TRY -WITH-RESOURCES)
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭