20

在 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 线程是您自己的责任。

4

4 回答 4

21

您不应该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 编程中罕见的极端情况的方法?

于 2018-10-19T09:11:02.303 回答
3

正如Elliott 在评论中指出的那样,随着 Java9+ 的发展,不推​​荐Object.finalize使用Cleaner. 此外,从发行说明:

java.lang.Object.finalize方法已被弃用。终结机制本质上是有问题的,并且可能导致性能问题、死锁和挂起。和提供更灵活java.lang.ref.Cleanerjava.lang.ref.PhantomReference更有效的方式来在对象变得不可访问时释放资源。

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

于 2018-10-18T17:57:32.713 回答
-1

两者都不使用。

正如 Holger 所提到的,试图从资源泄漏中恢复所面临的Cleaner挑战几乎与最糟糕的挑战一样多finalize,即过早完成(这不仅是finalize所有类型的软/弱/幻像引用的问题,也是所有类型的问题)。即使您尽最大努力正确地实现终结(同样,我指的是任何使用软/弱/幻像引用的系统),您也永远无法保证资源泄漏不会导致资源耗尽。不可避免的事实是 GC 不知道您的资源。

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

资源泄漏必须得到修复——不能解决。

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

于 2018-10-21T00:57:40.187 回答
-5

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

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

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

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

但是,finalize已被弃用。就是这样,我猜。有点鸡巴的举动。或许 JDK 开发者在想,“JDK 为什么要提供一个可以轻松实现为库的原生机制” “n00bs.n00bs 无处不在。n00bs,停止使用 finalize,我们非常讨厌你。” 这是一个很好的观点——然而,我无法想象finalize实际上会消失。

可以在此处找到一篇讨论终结和概述替代终结如何工作的好文章:如何处理 Java 终结的内存保留问题它概括地描绘了 Cleaner 的工作原理。

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

Netty 的ResourceLeakDetectorCleaner.

于 2018-10-18T19:40:09.837 回答