首页 > 解决方案 > Java 9 Cleaner 是否应该优于最终化?

问题描述

在 Java 中,重写该finalize方法会得到不好的评价,尽管我不明白为什么。像这样的类FileInputStream使用它来确保close在 Java 8 和 Java 10 中被调用。然而,Java 9 引入了java.lang.ref.Cleaner它使用 PhantomReference 机制而不是 GC 终结。起初,我认为这只是为第三方类添加终结的一种方式。但是,在其 javadoc中给出的示例显示了一个可以使用终结器轻松重写的用例。

我应该finalize根据 Cleaner 重写我的所有方法吗?(当然,我没有很多。只是一些使用操作系统资源的类,特别是用于 CUDA 互操作。)

据我所知,Cleaner(通过 PhantomReference)避免了finalizer. 特别是,您无权访问已清理的对象,因此您无法复活它或它的任何字段。

但是,这是我能看到的唯一优势。清洁剂也很重要。事实上,它和 finalization 都使用了ReferenceQueue! (难道你不喜欢阅读 JDK 是多么容易吗?)它比定稿更快吗?它是否避免等待两个 GC?如果许多对象排队等待清理,它会避免堆耗尽吗?(在我看来,所有这些的答案是否定的。)

最后,实际上没有什么可以保证阻止您在清理操作中引用目标对象。请仔细阅读长 API 说明!如果你最终引用了该对象,整个机制将默默地中断,不像最终化总是试图跛行。最后,虽然终结线程由 JVM 管理,但创建和持有 Cleaner 线程是您自己的责任。

标签: javajava-9finalizerfinalizefinalization

解决方案


您不应该finalize()Cleaner. 方法的弃用和 (a )finalize()的引入发生在同一个 Java 版本中的事实仅表明发生了关于该主题的一般工作,而不是应该替代另一个。publicCleaner

该 Java 版本的其他相关工作是删除了 aPhantomReference不会自动清除的规则(是的,在 Java 9 之前,使用 aPhantomReference而不是finalize()仍然需要两个 GC 周期来回收对象)并引入Reference.reachabilityFence(…).

的第一个替代方案finalize()是根本没有垃圾收集相关操作。当你说你没有很多时这很好,但我finalize()在野外看到了完全过时的方法。问题是,这finalize()看起来是一种普通的protected方法,而作为某种破坏者的顽固神话finalize()仍然在一些互联网页面上传播。将其标记为已弃用可以向开发人员发出信号,表明情况并非如此,而不会破坏兼容性。使用需要显式注册的机制有助于理解这不是正常的程序流程。当它看起来比覆盖单个方法更复杂时,它不会受到伤害。

如果您的类确实封装了非堆资源,文档说明:

实例持有非堆资源的类应该提供一种方法来启用这些资源的显式释放,并且如果适当,它们还应该实现AutoCloseable

(所以这是首选的解决方案)

CleanerPhantomReference提供了更灵活、更有效的方法来在对象变得无法访问时释放资源。

因此,当您真正需要与垃圾收集器交互时,即使是这个简短的文档注释也列出了两个替代方案,因为PhantomReference这里没有提到开发人员隐藏的后端Cleaner;usingPhantomReference是 的替代方法Cleaner,使用起来可能更复杂,但也提供了对时间和线程的更多控制,包括在使用资源的同一线程内进行清理的可能性。(与WeakHashMap相比,它有这样的清理避免了线程安全构造的开销)。它还允许处理在清理过程中抛出的异常,以比默默吞下它们更好的方式。

但甚至Cleaner可以解决更多您知道的问题。

一个重要的问题是注册时间。

  • 执行构造函数时,注册具有非平凡finalize()方法的类的对象。Object()此时,对象还没有被初始化。如果您的初始化因异常而终止,该finalize()方法仍将被调用。通过对象的数据来解决这个问题可能很诱人,例如将initialized标志设置为true,但您只能对您自己的实例数据这么说,而对于子类的数据则不能这样说,子类的数据在您的构造函数返回时仍未初始化。

    注册一个清理器需要一个完整的构造器Runnable,它包含清理所需的所有数据,而不需要引用正在构造的对象。您甚至可以在构造函数中没有发生资源分配时推迟注册(想想未绑定的Socket实例或未Frame原子连接到显示器的实例)

  • 可以finalize()重写方法,而无需调用超类方法或在异常情况下无法执行此操作。通过声明它来防止方法被覆盖,final根本不允许子类有这样的清理动作。相反,每个班级都可以注册清洁工,而不会干扰其他清洁工。

当然,您可以使用封装对象解决此类问题,但是,finalize()为每个类提供一个方法的设计被引导到另一个错误的方向。

  • 正如您已经发现的那样,有一种clean()方法可以立即执行清理操作并删除清洁器。所以在提供显式关闭方法甚至实现AutoClosable时,这是清理的首选方式,及时处置资源,摆脱基于垃圾收集器清理的所有问题。

    请注意,这与上述要点相协调。一个对象可以有多个清理器,例如由层次结构中的不同类注册。它们中的每一个都可以单独触发,具有关于访问权限的内在解决方案,只有注册了清洁器的人才能获得关联Cleanable的人才能调用该clean()方法。


也就是说,经常被忽视的是,在使用垃圾收集器管理资源时可能发生的最糟糕的事情并不是清理操作可能会稍后运行或根本不会运行。可能发生的最糟糕的事情是它运行得太早了。例如,请参阅Java 8 中对强可达对象调用的 finalize()。或者,一个非常好的,JDK-8145304,Executors.newSingleThreadExecutor().submit(runnable) 抛出 RejectedExecutionException,其中终结器关闭仍在使用的执行器服务。

当然,只是使用CleanerPhantomReference不解决这个问题。但是在真正需要时删除终结器并实施替代机制是一个仔细考虑主题并可能在需要的地方插入reachabilityFences的机会。您可能拥有的最糟糕的事情是一种看起来易于使用的方法,而实际上,该主题非常复杂,并且其 99% 的使用可能有一天会中断。

此外,虽然替代方案更复杂,但您自己说过,它们很少需要。这种复杂性只会影响您代码库的一小部分。为什么java.lang.Object所有类的基类都应该承载一个解决 Java 编程中罕见的极端情况的方法?


推荐阅读