Effective第三版 中英 | 避免使用终结方法和清理方法

Effective第三版

在这里插入图片描述

前言

大家好,这里是 Rocky 编程日记 ,喜欢后端架构及中间件源码,目前正在阅读 effective-java 书籍。同时也把自己学习该书时的笔记,代码分享出来,供大家学习交流,如若笔记中有不对的地方,那一定是当时我的理解还不够,希望你能及时提出。如果对于该笔记存在很多疑惑,欢迎和我交流讨论,最后也感谢您的阅读,点赞,关注,收藏~

前人述备矣,我只是知识的搬运工,effective 书籍源码均在开源项目 java-diary 中的 code-effective-third 模块中

源代码仓库地址: https://gitee.com/Rocky-BCRJ/java-diary.git

避免使用终结方法和清理方法(Avoid finalizers and cleaners)

  Finalizers are unpredictable, often dangerous, and generally unnecessary. Their use can cause erratic behavior, poor performance, and portability problems. Finalizers have a few valid uses, which we’ll cover later in this item, but as a rule, you should avoid them. As of Java 9, finalizers have been deprecated, but they are still being used by the Java libraries. The Java 9 replacement for finalizers is cleaners. Cleaners are less dangerous than finalizers, but still unpredictable, slow, and generally unnecessary.

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

  C++ programmers are cautioned not to think of finalizers or cleaners as Java’s analogue of C++ destructors. In C++, destructors are the normal way to reclaim the resources associated with an object, a necessary counterpart to constructors. In Java, the garbage collector reclaims the storage associated with an object when it becomes unreachable, requiring no special effort on the part of the programmer. C++ destructors are also used to reclaim other nonmemory resources. In Java, a try-with-resources or try-finally block is used for this purpose (Item 9).

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

  One shortcoming of finalizers and cleaners is that there is no guarantee they’ll be executed promptly [JLS, 12.6]. It can take arbitrarily long between the time that an object becomes unreachable and the time its finalizer or cleaner runs. This means that you should never do anything time-critical in a finalizer or cleaner.For example, it is a grave error to depend on a finalizer or cleaner to close files because open file descriptors are a limited resource. If many files are left open as a result of the system’s tardiness in running finalizers or cleaners, a program may fail because it can no longer open files.

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

  The promptness with which finalizers and cleaners are executed is primarily a function of the garbage collection algorithm, which varies widely across implementations. The behavior of a program that depends on the promptness of finalizer or cleaner execution may likewise vary. It is entirely possible that such a program will run perfectly on the JVM on which you test it and then fail miserably on the one favored by your most important customer.

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

  Tardy finalization is not just a theoretical problem. Providing a finalizer for a class can arbitrarily delay reclamation of its instances. A colleague debugged a long-running GUI application that was mysteriously dying with an OutOfMemoryError. Analysis revealed that at the time of its death, the application had thousands of graphics objects on its finalizer queue just waiting to be finalized and reclaimed. Unfortunately, the finalizer thread was running at a lower priority than another application thread, so objects weren’t getting finalized at the rate they became eligible for finalization.

  延迟终结过程并不只是一个理论问题。为类提供终结方法可以延迟其实例的回收过程。一位同事在调试一个长期运行的 GUI 应用程序的时候,该应用程序莫名其妙地出现 OutOfMemoryError 错误而死亡。分析表明,该应用程序死亡的时候,其终结方法队列中有数千个图形对象正在等待被回收和终结。遗憾的是,终结方法所在的线程优先级比应用程序其他线程的要低得多,所以对象没有在符合回收条件的时候及时被回收。

  The language specification makes no guarantees as to which thread will execute finalizers, so there is no portable way to prevent this sort of problem other than to refrain from using finalizers. Cleaners are a bit better than finalizers in this regard because class authors have control over their own cleaner threads, but cleaners still run in the background, under the control of the garbage collector, so there can be no guarantee of prompt cleaning.
  语言规范并不保证哪个线程将会执行终结方法,所以,除了避免使用终结方法之外,并没有很轻便的办法能够避免这样的问题。在这方面,清理方法比终结方法要好一些,因为类的创建者可以控制他们自己的清理线程,但是清理方法仍然是在后台运行,还是在垃圾收集器的控制下,因此无法保证及时清理。

  Not only does the specification provide no guarantee that finalizers or cleaners will run promptly; it provides no guarantee that they’ll run at all. It is entirely possible, even likely, that a program terminates without running them on some objects that are no longer reachable. As a consequence, you should never depend on a finalizer or cleaner to update persistent state.For example, depending on a finalizer or cleaner to release a persistent lock on a shared resource such as a database is a good way to bring your entire distributed system to a grinding halt.

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

  Don’t be seduced by the methods System.gc and System.runFinalization. They may increase the odds of finalizers or cleaners getting executed, but they don’t guarantee it. Two methods once claimed to make this guarantee: System.runFinalizersOnExit and its evil twin, Runtime.runFinalizersOnExit. These methods are fatally flawed and have been deprecated for decades [ThreadStop].

  不要被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]. Uncaught exceptions can leave other objects in a corrupt state. If another thread attempts to use such a corrupted object, arbitrary nondeterministic behavior may result.

  终结方法的另一个问题是忽略了在终止过程中被抛出的未捕获的异常,那么该对象的终结过程也会终止(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)。如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。

  Normally, an uncaught exception will terminate the thread and print a stack trace, but not if it occurs in a finalizer—it won’t even print a warning. Cleaners do not have this problem because a library using a cleaner has control over its thread.

  正常情况下,未被捕获的异常将会使线程终止,并打印出堆栈信息,但是,如果异常发生在终止过程中,则不会如此,甚至连警告都不会打印出来。清理方法就不会有这种问题,因为使用清理方法的库可以控制其所在的线程。

  There is a severe performance penalty for using finalizers and cleaners. On my machine, the time to create a simple AutoCloseable object, to close it using try-with-resources, and to have the garbage collector reclaim it is about 12 ns. Using a finalizer instead increases the time to 550 ns. In other words, it is about 50 times slower to create and destroy objects with finalizers. This is primarily because finalizers inhibit efficient garbage collection. Cleaners are comparable in speed to finalizers if you use them to clean all instances of the class (about 500 ns per instance on my machine), but cleaners are much faster if you use them only as a safety net, as discussed below. Under these circumstances, creating, cleaning, and destroying an object takes about 66 ns on my machine, which means you pay a factor of five (not fifty) for the insurance of a safety net if you don’t use it.

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

  Finalizers have a serious security problem: they open your class up to finalizer attacks.

  终结方法有一个很严重的安全问题:它们会打开你的类直到终结方法对其进行攻击。

  The idea behind a finalizer attack is simple: If an exception is thrown from a constructor or its serialization equivalents—the readObject and readResolve methods (Chapter 12)—the finalizer of a malicious subclass can run on the partially constructed object that should have “died on the vine.”

  This finalizer can record a reference to the object in a static field, preventing it from being garbage collected. Once the malformed object has been recorded, it is a simple matter to invoke arbitrary methods on this object that should never have been allowed to exist in the first place.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.Such attacks can have dire consequences.

   Final classes are immune to finalizer attacks because no one can write a malicious subclass of a final class.To protect nonfinal classes from finalizer attacks, write a final finalize method that does nothing.

  使用终结方法进行攻击的原理很简单(The idea behind a finalizer attack is simple):如果从构造方法或将其序列化的等价方法(readObject 和 readResolve[第 12 章])中抛出异常,恶意子类的终结方法可以在部分构造的对象上运行,这些对象应该“死在藤上(died on the vine)”。

  这些终结方法可以在一个静态域上记录下这些对象的引用,保护它们不被垃圾回收器回收。一旦这些异常的对象被记录下来,在这个对象上调用任意方法是一件简单的事情,这些方法本来就不应该被允许存在。从构造函数中抛出异常应足以防止对象的创建,在终结方法中,事实并非如此。

  这种攻击会产生可怕的后果。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? Just have your class implement AutoCloseable, and require its clients to invoke the close method on each instance when it is no longer needed, typically using try-with-resources to ensure termination even in the face of exceptions (Item 9). One detail worth mentioning is that the instance must keep track of whether it has been closed: the close method must record in a field that the object is no longer valid, and other methods must check this field and throw an IllegalStateException if they are called after the object has been closed.

  某些类(比如文件或线程)封装了需要终止的资源,对于这些类的对象,你应该用什么方法来替代终结方法和清理方法呢?(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(如果其他方法在实例关闭之后被调用)。

  So what, if anything, are cleaners and finalizers good for? They have perhaps two legitimate uses. One is to act as a safety net in case the owner of a resource neglects to call its close method. While there’s no guarantee that the cleaner or finalizer will run promptly (or at all), it is better to free the resource late than never if the client fails to do so. If you’re considering writing such a safety-net finalizer, think long and hard about whether the protection is worth the cost. Some Java library classes, such as FileInputStream, FileOutputStream, ThreadPoolExecutor, and java.sql.Connection, have finalizers that serve as safety nets.

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

  A second legitimate use of cleaners concerns objects with native peers. A native peer is a native (non-Java) object to which a normal object delegates via native methods. Because a native peer is not a normal object, the garbage collector doesn’t know about it and can’t reclaim it when its Java peer is reclaimed. A cleaner or finalizer may be an appropriate vehicle for this task, assuming the performance is acceptable and the native peer holds no critical resources. If the performance is unacceptable or the native peer holds resources that must be reclaimed promptly, the class should have a close method, as described earlier.

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

  Cleaners are a bit tricky to use. Below is a simple Room class demonstrating the facility. Let’s assume that rooms must be cleaned before they are reclaimed. The Room class implements AutoCloseable; the fact that its automatic cleaning safety net uses a cleaner is merely an implementation detail. Unlike finalizers, cleaners do not pollute a class’s public API:

  清理方法使用起来有一点棘手。下面是一个使用 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();
    }
}

  The static nested State class holds the resources that are required by the cleaner to clean the room. In this case, it is simply the numJunkPiles field, which represents the amount of mess in the room. More realistically, it might be a final long that contains a pointer to a native peer. State implements Runnable, and its run method is called at most once, by the Cleanable that we get when we register our State instance with our cleaner in the Room constructor. The call to the run method will be triggered by one of two things: Usually it is triggered by a callto Room’s close method calling Cleanable’s clean method. If the client fails to call the close method by the time a Room instance is eligible for garbage collection, the cleaner will (hopefully) call State’s run method.

  静态嵌套 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)。

总结

大家好,这里是 Rocky 编程日记 ,喜欢后端架构及中间件源码。如果对于该笔记存在很多疑惑,欢迎评论区探讨,最后也感谢您的阅读,点赞,关注,收藏~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值