第8项:避免使用终结方法和清空方法

??终结方法是不可预测的,通常很危险,一般情况下是不必要的(Finalizers are unpredictable, often dangerous, and generally unnecessary.)。使用 终结方法会导致行为不稳定,降低性能,以及可移植性问题。当然,终结方法也有可用之处,我们将在本项的最后再做介绍;但是,作为一项规则,我们应该避免使用它们。在Java 9 中,终结方法已经过时了,但是在Java库中还在使用。Java 9中替代终结方法的方式是清理方法。清理方法比终结方法危险性更低,但仍然是不可预测的,性能低,而且是不必要的(Cleaners are less dangerous than finalizers, but still unpredictable,
slow, and generally unnecessary)。

??提醒C ++程序员不要将终结方法或清理方法视为Java的C ++析构函数的类比。 在C ++中,析构函数是回收与对象关联的资源的常用方法,对象是构造函数的必要对应物。 在Java中,当一个对象无法访问时,垃圾回收器会回收与对象相关联的内存,而不需要程序员的特别处理(requiring no special effort on the part of the programmer)。C++的析构函数也可以被用来回收其他的非内存资源。在Java中,使用try-with-resources或者try-finally块来完成这个目的。

??终结方法或者清理方法的缺点在于不能保证会被及时地执行[JLS, 12.6]。从一个对象变得不可达开始,到它的终结方法或清理方法被执行,所花费的这段时间是任意长的(也就是说我们无法预知一个对象在销毁之后和执行终结方法和清理方法之间的间隔时间)。这意味着,对时间有严格要求(time-critical)的任务不应该由终结方法或清理方法来完成。例如,用中介方法来关闭已经打开的文件,这是严重的错误,因为打开文件的描述符是一种有限的资源。如果由于系统在运行终结方法或清理方法时延迟而导致许多文件处于打开状态,则程序可能会因为无法再打开文件而运行失败。

??执行终结算法和清除方法的及时性主要取决于垃圾回收算法,垃圾回收算法在不同的JVM实现中大相径庭。如果程序依赖于终结方法或清理方法被执行的时间点,这个程序可能在你测试它的JVM上完美运行,然而在你最重要客户的JVM平台上却运行失败,这完全是有可能的。

??延迟终结过程并不只是一个理论问题。为类提供终结方法可以延迟其实例的回收过程。一位同事在调试一个长期运行的GUI应用程序的时候,该应用程序莫名其妙地出现OutOfMemoryError错误而死亡。分析表明,该应用程序死亡的时候,其终结方法队列中有数千个图形对象正在等待被回收和终结。遗憾的是,终结方法所在的线程优先级比应用程序其他线程的要低得多,所以对象没有在符合回收条件的时候及时被回收( so objects were not getting finalized at the rate they became eligible for finalization)。语言规范并不保证哪个线程将会执行终结方法,所以,除了避免使用中介方法之外,并没有很轻便的办法能够避免这样的问题。在这方面,清理方法比终结方法要好一些,因为类的创建者可以控制他们自己的清理线程,但是清理方法仍然是在后台运行,还是在垃圾收集器的控制下,因此无法保证及时清理。

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

??不要被System.gcSystem.runFinalization这两个方法所诱惑,他们确实增加了终结方法和清理方法被执行的机会,但是他们不保证终结方法或清理方法一定会被执行。唯一声称保证这两个方法一定会被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了[ThreadStop]。

??终结方法的另一个问题是忽略了在终止过程中被抛出的未捕获的异常,那么该对象的终结过程也会终止(Another problem with finalizers is that an uncaught exception thrown during finalization is ignored, and finalization of that object terminates)[JLS, 12.6]。未捕获的异常会使对象处于破坏的状态(a corrupt state)。如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未被捕获的异常将会使线程终止,并打印出堆栈信息,但是,如果异常发生在终止过程中,则不会如此,甚至连警告都不会打印出来。清理方法就不会有这种问题,因为使用清洁方法的库可以控制其所在的线程。

??使用终结方法和清理方法会严重影响性能。在我的机器上,创建一个简单的AutoCloseable对象,使用try-with-resources关闭它,并让垃圾收集器回收它的时间大约是12 ns。使用终结方法之后时间增加到550ns。换句话说,用终结方法创建和销毁对象慢了大约50倍。这主要是因为终结器会抑制有效的垃圾收集。如下所述,如果你使用清洁方法或终结方法去清理类的所有实例,清理方法和终结方法的速度是差不多的(在我的机器上每个实例大约500ns),但是如果你只是把这两个方法作为安全保障(safety net)的话,清理方法比终结方法快很多。在这种情况下,在我的机器上创建,清理d和销毁一个对象大约需要66 ns,这意味着如果你不适用它,你需要支付五倍(而不是五十)安全保障的费用(which means you pay a factor of five (not fifty) for the insurance of a safety net if you don’t use it)。

??终结方法有一个很严重的安全问题:它们会打开你的类直到终结方法对其进行攻击(they open your class up to finalizer attacks)。使用终结方法进行攻击的原理很简单(The idea behind a finalizer attack is simple):如果从构造方法或将其序列化的等价方法(readObject和readResolve[第12章])中抛出异常,恶意子类的终结方法可以在部分构造的对象上运行,这些对象应该“死在藤上(died on the vine)”。这些终结方法可以在一个静态域上记录下这些对象的引用,保护它们不被垃圾回收器回收。一旦这些异常的对象呗记录下来,在这个对象上调用任意方法是一件简单的事情,这些方法本来就不应该被允许存在。从构造函数中抛出异常应足以防止对象的创建,在终结方法中,事实并非如此(Throwing an exception from a constructor should be sufficient to prevent an object from coming into existence; in the presence of finalizers, it is not)。这种攻击会产生可怕的后果。final修饰的类不会受到终结方法的攻击,因为没人可以编写final类的恶意子类。要保护非final类受到终结方法的攻击,请编写一个不执行任何操作的final finalize方法。

??某些类(比如文件或线程)封装了需要终止的资源,对于这些类的对象,你应该用什么方法来替代终结方法和清理方法呢?(So what should you do instead of writing a finalizer or cleaner for a class whose objects encapsulate resources that require termination, such as files or threads?)对于这些类,你只需要让其实现AutoCloseable接口,并要求其客户端在每个实例不再需要的时候调用实例上的close方法,通常使用try-with-resources来确保即使出现异常时资源也会被终止(第9项)。值得一提的一个细节是实例必须跟踪其本身是否已被关闭:close方法必须在一个字段中记录这个实例已经无效,而其他方法必须检查此字段并抛出IllegalStateException(如果其他方法在实例关闭之后被调用)。

??那么清理方法和终结方法有什么作用呢?它们可能有两种合理的用途。第一种用途是,当对象的所有者忘记调用其终止方法的情况下充当安全网(safety net)。虽然不能保证清理方法或终结方法能够及时调用(或者根本不运行),晚一点释放关键资源总比永远不释放要好。如果你正在考虑编写这样的一个安全网终结方法,就要考虑清楚,这种额外的保护是否值得你付出这份额外的代价。某些Java类库(如FileInputStream、FileOutputStream、ThreadPoolExecutor、和java.sql.Connection)具有充当安全网终结方法。

??清理方法的第二个合理用途与对象的本地对等体(native peers)有关。本地对等体是普通对象通过本机方法委托的本机(非Java)对象,因为本地对等体不是普通对象,因此垃圾收集器不会知道它,并且在回收Java对等体时无法回收它。假设性能可接受并且本地对等体没有关键资源,则清理方法或终结方法可以是用于该任务的适当工具。如果性能不可接受或者本机对等体拥有必须回收的资源,则该类应该具有close方法,这正如之前所说的。

??清理方法使用起来有一点棘手。下面是一个使用Room类简单演示。让我们假设在rooms回收之前必须进行清理。这个Room类实现了AutoCloseable接口;事实上,它的自动清理安全网采用的是清理方法的实现仅仅是一个实现细节(the fact that its automatic cleaning safety net uses a cleaner is merely an implementation detail)。跟终结方法不一样的是,清理方法不会污染类的公共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类包含清理程序清理Room所需的资源。 在这种情况下,它只是numJunkPiles字段,它表示room的混乱程度。 更现实的是,它可能是一个包含指向本地对等体的指针的final long。 State实现了Runnable,它的run方法最多被调用一次,当我们在Room构造函数中使用我们的清理器注册State实例时,我们得到了Cleanable。 对run方法的调用将由以下两种方法之一触发:通常是通过调用Room的close方法调用Cleanable的clean方法来触发。 如果客户端无法在Room实例符合垃圾收集条件时调用close方法,则清理器将(希望)调用State的run方法。

??State实例不引用其Room实例至关重要。 如果是这样,它将创建一个循环,以防止Room实例符合垃圾收集的资格(以及自动清理)。 因此,State必须是静态嵌套类,因为非静态嵌套类包含对其封闭实例的引用(第24项)。 使用lambda同样不可取,因为它们可以轻松捕获对封闭对象的引用。

??正如我们之前所说,Room的清洁剂仅用作安全网。 如果客户端在try-with-resource块中包围所有Room实例,则永远不需要自动清理。 这个表现良好的客户端演示了这种行为:

public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("Goodbye");
        }
    }
}

??正如你所期望的那样,运行Adult程序会打印Goodbye,然后是Cleaning Room。 但是,这个永远不会清理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,但不能保证你会在你的机器上看到相同的行为。

??总之,除了作为安全网或终止非关键的本机资源之外,不要使用清理方法,也不要使用Java 9之前的版本(终结方法)。即使这样,也要注意不确定性和影响性能导致的后果(Even then, beware the indeterminacy and performance consequences)。

关注个人公众号获取更新!

原文地址:https://www.cnblogs.com/coloured_glaze/p/10661860.html

时间: 2024-10-31 01:39:09

第8项:避免使用终结方法和清空方法的相关文章

C# static 字段初始值设定项无法引用非静态字段、方法或属性

问题:字段或属性的问题字段初始值设定项无法引用非静态字段.方法 下面代码出错的原因,在类中定义的字段为什么不能用? public string text = test(); //提示 字段或属性的问题字段初始值设定项无法引用非静态字段.方法 protected void Page_Load(object sender, EventArgs e) { } public string test() { return ""; } 可以通过属性方式调用: public string text

Array的队列方法&重排序方法—— JS学习笔记2015-6-27(第68天)

队列方法 相对于栈数据结构的后进先出[LIFO]访问规则,队列数据结构的访问规则是先进先出[FIFO] 这里提到一个方法  shift(); 它能够移除数组中的第一个项,并返回该项,同时将数组长度减1:// 有点像pop() 实例: var colors = ['red','green']; var item = colors.shift(); alert(colors);  // green alert(item);  // red alert(colors.length)  // 1; 同时

java面向对象编程(六)--四大特征之继承、方法重载和方法覆盖

一.继承 1.继承的概念 继承可以解决代码复用,让我们的编程更加靠近人类思维.当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends语句来声明继承父类.语法如下: class 子类 extends 父类 这样,子类就会自动拥有父类定义的某些属性和方法.另外,并不是父类的所有属性.方法都可以被子类继承.父类的public修饰符的属性和方法,protected修饰符的属性和方法,默认修饰符属

JS高程5.引用类型(6)Array类型的位置方法,迭代方法,归并方法

一.位置方法 ECMAScript5为数组实例添加了两个位置:indexOf()和 lastIndexOf().这两个方法接收两个参数:要查找的项和(可选的)表示查找起点位置的索引(如在数组[7,8,9,1,0]中,"7"在第一个位置,它的索引是0.).其中,indexOf()方法从数组的开头(位置0)开始向后查找,lastIndexOf()方法从数组的末尾开始向前查找. 注意: 这两个方法都返回要查找的项在数组中的位置,在没有找到的情况下返回-1. 在比较第一个参数与数组中的每一项时

java多态的2种表现形式 方法重载和方法覆盖

方法重载:同一个类中,方法名相同,参数列表不同的2个或多个方法构成方法的重载. 方法覆盖:子类重新实现了父类中的方法. 1.方法的重载实例(Overload) 指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法, 然后再调用时,VM就会根据不同的参数样式,来选择合适的方法执行 /** * 方法重载满足的条件 * 1.同一个类中,方法名相同,参数列表不同的2个或多个方法构成方法的重载 * 2.参数列表不同指参数的类型,参数的个数,参数的顺序至少一项不同 * 3.方法的返回值类型

绩效管理的改善方法与考核方法

绩效管理的改善方法与考核方法 作者:张国祥 2014年8月15日 说明:本文主要内容摘自笔者即将出版的专著<公司化运作指南>上篇管理体系建设第七章. 绩效是工作的有效成果.员工绩效就是员工工作的有效成果,企业绩效就是企业组织有效业绩的总和. 所谓绩效管理就是对绩效目标设立.达成.评价.运用.提升的循环管理过程. 企业管理本质上就是绩效管理.采用什么方法提高绩效管理水平因企业而异. 本文主要介绍绩效改善方法和考核方法. 一.绩效改善方法 绩效已经产生,怎么考核都无法改变结果.只有改变绩效产生的过

方法阻塞,方法一直阻塞,意味着这个程序卡在这里,一直不向下运行。知道这个阻塞方法执行完毕,有返回值。程序才继续向下执行

方法阻塞,方法一直阻塞,意味着这个程序卡在这里,一直不向下运行.知道这个阻塞方法执行完毕,有返回值.程序才继续向下执行. while (true) { // 当注册事件到达时,方法返回,否则该方法会一直阻塞 selector.select();  //这里将一直阻塞,程序不会向下执行.直到这个方法执行完,有返回值后 2.// 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 NIO SERVER NIO SERVERpackage com.anders.selecto

Java方法继承、方法重载、方法覆盖小总结

转自:http://blog.csdn.net/cdsnmdl/article/details/3968688 ———————————————————————————————————— 1.方法继承:利用extends关键字一个方法继承另一个方法,而且只能直接继承一个类. 当Sub类和Base类在同一个包时Sub类继承Base类中的public/protected/默认级别的变量个方法 在不同包时继承public/protected级别的变量和方法. 2.方法重载:如果有两个方法的方法名相同,但

ThinkPHP 跨模块调用操作方法(A方法与R方法)

ThinkPHP 跨模块调用操作方法(A方法与R方法) 跨模块调用操作方法 前面说了可以使用 $this 来调用当前模块内的方法,但实际情况中还经常会在当前模块调用其他模块的方法.ThinkPHP 内置了 A方法与 R 方法这两个特殊的大写字母方法来处理跨模块调用的问题. 目前 Index 模块内有 index 操作,User 模块有 showName 操作,User 模块及 showName 操作具体代码如下: <?php class UserAction extends Action{ pu