Java 9 Cleaner应该优先完成吗?

Ale*_*sky 14 java finalize finalization finalizer java-9

在Java中,覆盖该finalize方法会得到一个糟糕的说唱,虽然我不明白为什么.类似于FileInputStream使用它来确保close在Java 8和Java 10中调用.然而,Java 9引入java.lang.ref.Cleaner了使用PhantomReference机制而不是GC终结.起初,我认为这只是向第三方类添加finalization的一种方式.但是,其javadoc中给出的示例显示了一个可以使用终结器轻松重写的用例.

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

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

然而,这是我能看到的唯一优势.清洁工也是非平凡的.事实上,它和终结都使用了ReferenceQueue!(难道你不喜欢阅读JDK是多么容易吗?)它比完成更快吗?它是否避免等待两个GC?如果许多对象排队等待清理,它会避免堆耗尽吗?(所有这些的答案在我看来都不是.)

最后,实际上没有任何保证阻止您在清理操作中引用目标对象.小心阅读长API注意!如果你最终引用了这个对象,那么整个机制将会默默地中断,而不像终结总是试图跛行.最后,虽然终结线程由JVM管理,但创建和保存Cleaner线程是您自己的责任.

Hol*_*ger 14

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

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

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

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

其实例包含非堆资源的类应该提供一种方法来启用这些资源的显式释放,并且它们还应该在适当时实现AutoCloseable.

(这是首选的解决方案)

当对象无法访问时,CleanerPhantomReference提供了更灵活,更有效的方法来释放资源.

因此,当您真正需要与垃圾收集器进行交互时,即使是这个简短的文档注释也会列出两个备选方案,因为PhantomReference这里没有提到隐藏的开发人员后端Cleaner; PhantomReference直接使用是一种替代方法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编程的罕见问题呢?

  • 那么,`finalize()`应该永远不会存在.承认某些事情是个坏主意永远不会太迟.关于,'WeakReference`与`PhantomReference`,当我真正需要这样的东西时,我也使用了'WeakReference`.如上所述,在Java 9之前,幻像引用不会自动清除,需要在第二个周期中收集引用,这使得效率降低.除此之外,唯一的区别是对象必须在幻像引用入队之前完成,所以当你不使用终结器时,就没有区别了. (8认同)

Ale*_*sky -2

两者都不用。

尝试从资源泄漏中恢复使用所Cleaner带来的挑战几乎与finalizeHolger 提到的最糟糕的挑战一样多,其中最糟糕的是过早完成(这不仅是finalize软引用/弱引用/幻像引用的问题)。即使您尽最大努力正确实现终结(并且,我的意思是任何使用软/弱/幻像引用的系统),您也永远无法保证资源泄漏不会导致资源耗尽。不可避免的事实是GC不知道你的资源。

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

资源泄漏必须得到修复——而不是绕开。

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