Effective Java笔记(8)避免使用终结方法和清除方法

文章讨论了Java中终结方法(finalizer)和清除方法(cleaner)的使用风险,包括不可预测的执行时间、性能下降和可能导致的安全问题。建议避免使用终结方法,推荐实现AutoCloseable接口并通过try-with-resources进行资源管理。清除方法作为安全网在某些情况下可以接受,但仍有不确定性。文章强调了及时释放资源的重要性,尤其是对有限资源如文件描述符的管理。Java9以后,cleaner成为更安全的清理机制。
摘要由CSDN通过智能技术生成

        终结方法( finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的 。 使用终结方法会导致行为不稳定 、 性能降低,以及可移植性问题 。 当然,终结方法也有其可用之处,我们将在本条目的最后再做介绍;但是根据经验,应该避免使用终结方法 。 在 Java 9中用清除方法( cleaner )代替了终结方法 。 清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的 。

        终结方法和清除方法的缺点在于不能保证会被及时执行[JLS.12.6]。 从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的 。 这意味着, 注重时间的任务不应该由终结方法或者清除方法来完成 。 例如,用终结方法或者清除方法来关闭已经打开的文件,就是一个严重的错误,因为打开文件的描述符是一种很有限的资源 。 如果系统无法及时运行终结方法或者清除方法就会导致大量的文件仍然保留在打开状态,于是当一个程序再也不能打开文件的时候,它可能会运行失败 。

        及时地执行终结方法和清除方法正是垃圾回收算法的一个主要功能,这种算法在不同的 JVM 实现中会大相径庭 。 如果程序依赖于终结方法或者清除方法被执行的时间点,那么这个程序的行为在不同的 JVM 中运行的表现可能就会截然不同 。 一个程序在你测试用的JVM 平台上运行得非常好,而在你最重要顾客的 JVM 平台上却根本无法运行,这是完全有可能的 。

        延迟终结过程并不只是一个理论问题 。 在很少见的情况下,为类提供终结方法,可能会随意地延迟其实例的回收过程 。 一位同事最近在调试一个长期运行的 GUI 应用程序的时候,该应用程序莫名其妙地出现 OutOfMemoryError 错误而死掉 。 分析表明,该应用程序死掉的时候,其终结方法队列中有数千个图形对象正在等待被终结和回收 。 遗憾的是,终结方法线程的优先级比该应用程序的其他线程的优先级要低得多,所以,图形对象的终结速度达不到它们进入队列的速度 。Java 语言规范并不保证哪个线程将会执行终结方法,所以,除了不使用终结方法之外,并没有很轻便的办法能够避免这样的问题 。 在这方面,清除方法比终结方法稍好一些,因为类的设计者可以控制自己的清除线程, 但清除方法仍然在后台运行,处于垃圾回收器的控制之下,因此不能确保及时清除 。

        Java 语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行 。 当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的 。 结论是:永远不应该依赖终结方法或者清除方法来更新重要的持久状态 。 例如,依赖终结方法或者清除方法来释放共享资源(比如数据库)上的永久锁,这很容易让整个分布式系统垮掉 。

        不要被 System.gc 和 System.runFinalization 这两个方法所诱惑,它们确实增加了终结方法或者清除方法被执行的机会,但是它们并不保证终结方法或者清除方法一定会被执行 。 唯一声称保证它们会被执行的两个方法是 System.runFinalizersOnExit,及其臭名昭著的孪生兄弟 Runtime.runFinalizersOnExit 。 这两个方法都有致命的缺陷,井且已经被废弃很久了。

        使用终结方法的另一个问题是:如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。 未被捕获的异常会使对象处于破坏的状态( corrupt state ),如果另 一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为 。 正常情况下,未被捕获的异常将会使线程终止,并打印出战轨迹( Stack Trace ),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来 。 清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程 。

        使用终结方法和清除方法有一个非常严重的性能损失 。 在我的机器上,创建一个简单的 AutoCloseable 对象,用 try-with-resources 将它关闭,再让垃圾回收器将它回收,完成这些工作花费的时间大约为 12ns 。 增加一个终结方法使时间增加到了 550ns 。 换句话说,用终结方法创建和销毁对象慢了大约 50 倍 。 这主要是因为终结方法阻止了有效的垃圾回收 。 如果用清除方法来清除类的所有实例 ,它的速度 比终结方法会稍微快一些(在我的机器上大约是每个实例花 500ns ),但如果只是把清除方法作为一道安全网( safety net ),下面将会介绍,那么清除方法的速度还会更快一些 。 在这种情况下,创建、清除和销毁对象,在我的机器上花了大约 66ns ,这意味着,如果没有使用它,为了确保安全网多花了 5 倍(而不是 50 倍)的代价 。

        终结方法有一个严重的安全问题: 它们为终结方法攻击( finalizer attack ) 打开了类的大门 。 终结方法攻击背后的思想很简单:如果从构造器或者它的序列化对等体( readObject和 readResolve 方法)抛出异常,恶意子类的终结方法就可以在构造了 一部分的应该已经半途夭折的对象上运行 。 这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收 。 一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法 。 从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了 。 这种攻击可能造成致命的后果 。final类不会受到终结方法攻击,因为没有人能够编写出 final 类的恶意子类 。为了防止非 final 类受到终结方法攻击 , 要编写一个空的 final 的 finalize 方法 。

        那么,如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法或者清除方法呢?只需 让类实现 AutoCloseable,并要求其客户端在每个实例不再需要的时候调用 close 方法,一般是利用 try - with - resources 确保终止,即使遇到异常也是如此 。 值得提及的一个细节是,该实例必须记录下自己是否已经被关闭了:close 方法必须在一个私有域中记录下“该对象已经不再有效” 。 如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException 异常 。

        那么终结方法和清除方法有什么好处呢?它们有两种合法用途 。 第一种用途是,当资源的所有者忘记调用它的 close 方法时,终结方法或者清除方法可以充当样做井不能保证终结方法或者清除方法会被及时地运行,但是在客户端无法正常结束操作的情况下,迟一点释放资源总比永远不释放要好 。 如果考虑编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价 。 有些 Java 类(如 FileinputStream 、FileOutputStream 、ThreadPoolExecutor 和 java.sql.Connection )都具有能充当安全网的终结方法 。

        清除方法的使用有一定的技巧 。 下面以一个简单的 Room 类为例 。 假设房间在收回之前必须进行清除 。Room 类实现了 AutoCloseable ;它利用清除方法自动清除安全网的过程只不过是一个实现细节 。 与终结方法不同的是,清除方法不会污染类的公有 API:

        内嵌的静态类 State 保存清除方法清除房 间所需的资源 。 在这个例子中,就是 num-JunkPiles 域,表示房间的杂乱度 。 更现实地说,它可以 是 final 的 long,包含一个指向本地对等体的指针。State 实现了 Runnable 接口,它的 run 方法最多被 Cleanable调用一次 ,后者是我们在 Room 构造器中用清除器注册 State 实例时获得的 。 以下两种情况之一会触发 run 方法的调用:通常是通过调用 Room 的 close 方法触发的,后者又调用了 Cleanable 的清除方法 。 如 果到了 Room 实例应该被垃圾 回收时,客户端还没有调用close 方法,清除方法就会(希望如此 )调用 State 的 run 方法 。

        关键是 State 实例没有引用它的 Room 实例 。 如果它引用了,会造成循环,阻止 Room实例被垃圾回收 (以及防止被自动清除) 。因此 State 必须是一个静态的嵌套类,因为非静态的嵌套类包含了对其外围实例的引用 。 同样地,也不建议使用 lambda,因为它们很容易捕捉到对外围对象的引用 。

        如前所述, Room 的清除方法只用作安全网 。 如果客户端将所有的 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.print1n("Peace out");
    }
}

        你可能期望打印出 Peaceout ,然后是 Cleaning room ,但是在我 的机器 上,它没有打印出 Cleaning room ,就退出程序了 。 这就是我们之前提到过的不可预见性 。Cleaner 规范指出:“清除方法在 System.exit 期间的行为是与实现相关的 。 不确保清除动作是否会被调用 。 ”虽然规范没有指明,其实对于正常的程序退出也是如此 。 在我的机器上,只要在 Teenager 的 main 方法上添加代码行 System.gc(),就足以让它在退出之前打印出 Cleaning room ,但是不能保证在你的机器上也能看到相同的行为 。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值