Java 9 Cleaner 是否应该优先于 finalization?

Should Java 9 Cleaner be preferred to finalization?

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

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

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

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

最后,实际上没有什么可以保证阻止您在清理操作中引用目标对象。仔细阅读长API 注意!如果你最终引用了这个对象,整个机制就会悄无声息地崩溃,不像终结总是试图一瘸一拐地前进。最后,虽然终结线程由 JVM 管理,但创建和持有 Cleaner 线程是您自己的责任。

正如 所指出的,随着 Java9+ 的推进,Object.finalize 已被弃用,因此使用 Cleaner 实现方法更有意义。另外,从发行说明:

The java.lang.Object.finalize method has been deprecated. The finalization mechanism is inherently problematic and can lead to performance issues, deadlocks, and hangs. The java.lang.ref.Cleaner and java.lang.ref.PhantomReference provide more flexible and efficient ways to release resources when an object becomes unreachable.

Bug 数据库中的详细信息 - JDK-8165641

Java 9的Cleaner与传统的finalization非常相似(在OpenJDK中实现),关于finalization的几乎所有(好的或坏的)都可以说关于Cleaner。两者都依靠垃圾收集器将 Reference 个对象放在 ReferenceQueue 上,并使用单独的线程来 运行 清理方法。

三个主要区别是 Cleaner 使用 PhantomReference 而不是本质上的 WeakReference(幻象引用不允许您访问该对象,这确保它无法访问,即 zombified),每个 Cleaner 使用一个单独的线程和可定制的 ThreadFactory,并允许手动清除(即取消)PhantomReferences 并且从不排队。

这在大量使用 Cleaner/finalization 时提供性能优势。 (不幸的是,我没有基准来说明优势有多大。)但是,大量使用终结是不正常的。

对于 finalize 用于的正常事情——即,使用保持最小必要状态的小型最终对象实现的本机资源的最后手段清理机制,提供 AutoCloseable,并且没有每秒分配数百万——除了使用差异之外,这两种方法之间没有实际区别(在某些方面 finalize 更容易实现,在其他方面 Cleaner 有助于避免错误). Cleaner 不提供任何额外的保证或行为(例如保证 cleaners 将 运行 在进程退出之前——这基本上是不可能保证的)。

但是,finalize 已被弃用。 我猜就是这样。有点鸡巴的举动。也许 JDK 开发人员在想,“为什么 JDK 应该提供一种可以作为库轻松实现的本机机制” “n00bs.n00bs 无处不在。n00bs ,停止使用 finalize,我们非常讨厌你。”这是一个很好的观点——但是,我无法想象 finalize 真的消失了。

可以在此处找到一篇讨论终结并概述替代终结工作原理的好文章:How to Handle Java Finalization's Memory-Retention Issues它粗略地描绘了 Cleaner 的工作原理。

可能使用 Cleaner 或 PhantomReference 而不是 finalize 的代码示例是 Netty 的直接(非堆)内存的引用计数手动管理。在那里,分配了很多可终结的对象,Netty 采用的替代终结机制是有意义的。然而,Netty 更进一步,不会为每个引用计数对象创建引用,除非泄漏检测器设置为最高灵敏度。在通常的操作中,它要么根本不使用终结(因为如果有资源泄漏,无论如何你最终都会发现它)或使用采样(将清理代码附加到一小部分分配的对象) .

Netty 的 ResourceLeakDetectorCleaner 爽多了。

您不应将所有 finalize() 方法替换为 Cleanerfinalize() 方法的弃用和 (a public) Cleaner 的引入发生在同一个 Java 版本中,这一事实仅表明该主题的一般性工作发生了, 并不是说​​一个应该是另一个的替代品。

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

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

如果您的 class 确实封装了非堆资源,the documentation 声明:

Classes whose instances hold non-heap resources should provide a method to enable explicit release of those resources, and they should also implement AutoCloseable if appropriate.

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

The Cleaner and PhantomReference provide more flexible and efficient ways to release resources when an object becomes unreachable.

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

但即使是 Cleaner 也能解决更多您知道的问题。

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

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

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

  • A finalize() 方法可以被覆盖,无需调用 superclass 方法或在特殊情况下未能执行此操作。通过声明它 final 来防止方法被覆盖,根本不允许子 classes 有这样的清理操作。相比之下,每个 class 都可以注册清洁工而不会干扰其他清洁工。

诚然,您可以使用封装对象解决此类问题,但是,为每个 class 设计一个 finalize() 方法的设计会导致另一个错误的方向。

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

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


就是说,人们常常忽略了在使用垃圾收集器管理资源时可能发生的最糟糕的事情,并不是清理操作可能 运行 延迟或根本不执行。可能发生的最糟糕的事情是 运行太早。请参阅 finalize() called on strongly reachable object in Java 8 for example. Or, a really nice one, JDK-8145304, Executors.newSingleThreadExecutor().submit(runnable) throws RejectedExecutionException,其中终结器会关闭仍在使用的执行程序服务。

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

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

两个都不用。

尝试使用 Cleaner 从资源泄漏中恢复所面临的挑战几乎与 finalize 一样多,正如 Holger 所提到的,其中最糟糕的是过早完成(这不仅是 finalize 但有各种 soft/weak/phantom 参考)。即使您尽最大努力正确地实现终结(再次强调,我指的是任何一种使用 soft/weak/phantom 引用的系统),您也永远无法保证资源泄漏不会导致资源耗尽。不可避免的事实是 GC 不知道您的资源。

相反,您应该假设资源将被正确关闭(通过 AutoCloseabletry-with-resources、引用计数等),找到并修复错误而不是希望解决它们,并且仅将终结(以任何形式)用作调试辅助工具,很像 assert

必须修复资源泄漏——无法解决。

定稿仅应用作断言机制,以(尝试)通知您存在错误。为此,我建议看一下 Netty 派生的 almson-refcount。它提供了一个基于弱引用的有效资源泄漏 检测器 ,以及一个比通常的 AutoCloseable 更灵活的可选引用计数工具。它的泄漏检测器之所以出色,是因为它提供了不同级别的跟踪(具有不同的开销),您可以使用它来捕获泄漏对象分配和使用位置的堆栈跟踪。