在Java中,重写finalize
方法的说唱不好,尽管我不明白为什么。诸如FileInputStream
之类的类使用它来确保在Java 8和Java 10中都调用close
。但是,Java 9引入了java.lang.ref.Cleaner
,它使用PhantomReference机制而不是GC终结。起初,我认为这只是将终结处理添加到第三方类中的一种方法。但是,its javadoc中给出的示例显示了一个用例,可以很容易地用终结器重写。
我应该用Cleaner重写所有finalize
方法吗? (当然,我没有很多。只有一些使用OS资源的类,尤其是用于CUDA互操作的类。)
据我所知,Cleaner(通过PhantomReference)避免了finalizer
的某些危险。特别是,您无权访问已清除的对象,因此您无法复活该对象或其任何字段。
但是,这是我所能看到的唯一优势。清洁也很重要。实际上,它和终结处理都使用ReferenceQueue
! (您不只是喜欢阅读JDK多么容易吗?)它比定稿要快吗?是否避免等待两个GC?如果有许多对象排队进行清理,它将避免堆耗尽吗? (所有这些答案在我看来都是“否”。)
最后,实际上并不能保证阻止您在清理操作中引用目标对象。请仔细阅读较长的API注意!如果您最终引用了该对象,则整个机制将无声地中断,这与完成过程总是试图li行一样。最后,虽然终结处理线程是由JVM管理的,但是创建和保留Cleaner线程是您自己的责任。
答案 0 :(得分:13)
您不应将所有finalize()
方法替换为Cleaner
。 finalize()
方法的弃用和(public
)Cleaner
的引入是在同一Java版本中发生的事实,仅表明发生了有关该主题的常规工作,而不是应该可以替代对方。
该Java版本的其他相关工作是删除不自动清除PhantomReference
的规则(是的,在Java 9之前,仍然需要使用PhantomReference
而不是finalize()
两个GC周期回收该对象)并引入Reference.reachabilityFence(…)
。
finalize()
的第一种选择是完全不依赖垃圾回收。如果您说自己没有很多,那很好,但是我发现野蛮的finalize()
方法完全过时了。问题在于,finalize()
看起来像是一种普通的protected
方法,而顽固的关于finalize()
是某种析构函数的神话仍然散布在某些互联网页面上。将其标记为已弃用,可以向开发人员表示情况并非如此,而不会破坏兼容性。使用需要显式注册的机制有助于理解这不是正常的程序流程。而且它看起来比覆盖单个方法更复杂时也没有什么坏处。
如果您的类确实封装了非堆资源,则the documentation指出:
其实例拥有非堆资源的类应提供一种方法来启用这些资源的显式释放,并且在适当的情况下,它们还应实现AutoCloseable。
(这是首选的解决方案)
Cleaner和PhantomReference提供了一种更灵活,更有效的方法来在对象无法访问时释放资源。
因此,当您真正需要与垃圾收集器进行交互时,即使简短的文档注释也提供了两个替代方案,因为PhantomReference
并未被提及为{{ 1}}在这里;直接使用Cleaner
是PhantomReference
的替代方法,它可能使用起来更加复杂,而且还提供了对时序和线程的更多控制,包括在使用资源的同一线程中进行清理的可能性。 (与Cleaner
相比,它具有这样的清理功能,避免了线程安全构造的开销)。它也允许以比静静吞咽它们更好的方式处理清理期间抛出的异常。
但是,即使WeakHashMap
也解决了您所意识到的更多问题。
一个重要的问题是注册时间。
在执行Cleaner
构造函数后,将注册具有简单finalize()
方法的类的对象。此时,该对象尚未初始化。如果初始化异常终止,则Object()
方法仍将被调用。用对象的数据来解决这个问题可能很诱人,例如将finalize()
标志设置为initialized
,但是您只能针对自己的实例数据说这句话,而不能针对子类的数据说这话,子类的数据在构造函数返回时仍未初始化。
注册一个清理器需要一个完全构造的true
,其中包含用于清理的所有必要数据,而无需引用正在构造的对象。您甚至可以在构造函数中未进行资源分配时推迟注册(例如,未绑定的Runnable
实例或Socket
并没有原子地连接到显示器的实例)
可以覆盖Frame
方法,而无需调用超类方法或在特殊情况下不这样做。通过声明finalize()
来防止该方法被覆盖,根本不允许子类具有此类清除操作。相反,每个班级都可以注册清洁工,而不会干扰其他清洁工。
当然,您可以使用封装的对象解决此类问题,但是,可以为每个指向另一个错误方向的类使用final
方法。
您已经发现,有一个finalize()
方法,该方法可以立即执行清理操作并删除清理器。因此,当提供显式的close方法甚至实现clean()
时,这是首选的清理方式,及时处理资源并摆脱所有基于垃圾收集器的清理问题。
请注意,这与上述要点保持一致。一个物体可以有多个清洁剂,例如由层次结构中的不同类注册。它们中的每一个都可以使用有关访问权限的内在解决方案单独触发,只有注册了清洁程序的人才能使用关联的AutoClosable
才能调用Cleanable
方法。
也就是说,经常被忽略的是,使用垃圾收集器管理资源时可能发生的最糟糕的事情不是清理动作可能以后运行或根本不会运行。可能发生的最糟糕的事情是,它运行得太早了。例如,请参见finalize() called on strongly reachable object in Java 8。或者,一个非常好的JDK-8145304, Executors.newSingleThreadExecutor().submit(runnable) throws RejectedExecutionException,其中终结器关闭仍在使用的执行器服务。
仅使用clean()
或Cleaner
不能解决此问题。但是,在真正需要时删除终结器并实现替代机制是一个机会,可以仔细考虑该主题,并在需要时插入reachabilityFence
s。您可能遇到的最糟糕的事情是一种看起来易于使用的方法,而实际上,该主题非常复杂,其使用的99%可能有一天会中断。
此外,尽管替代方案更加复杂,但您自己说,很少需要它们。这种复杂性仅会影响代码库的一小部分。为什么PhantomReference
(所有类的基类)为什么要托管一个方法来解决Java编程的一种罕见情况?
答案 1 :(得分:3)
正如Elliott in comments所指出的那样,随着Java9 +的发展,Cleaner
已过时,因此使用java.lang.Object.finalize
实现方法更有意义。另外,从发行说明中:
不推荐使用
java.lang.ref.Cleaner
方法。的 最终确定机制本质上存在问题,并可能导致 性能问题,僵局和挂起。java.lang.ref.PhantomReference
和SendGrid
提供了更灵活,更有效的方法 当对象变得不可访问时释放资源的方法。
错误数据库中的详细信息-JDK-8165641
答案 2 :(得分:-2)
都不使用。
使用Cleaner
尝试从资源泄漏中恢复所面临的挑战几乎与finalize
一样多,正如Holger所言,最坏的挑战是过早的终结(不仅{{1 }},但包含各种软/弱/幻像引用)。即使您尽最大努力正确地实现了终结处理(再一次,我的意思是任何使用软/弱/幻象引用的系统),您都无法保证资源泄漏不会导致资源耗尽。不可避免的事实是GC不了解您的资源。
相反,您应该假设资源将被正确关闭,发现并修复错误,而不是希望解决这些错误,并且仅将finalization(以任何形式使用)用于调试。 >
为此,我建议看一下Netty衍生的almson-refcount。它基于弱引用提供了有效的资源泄漏检测器,并且提供了可选的引用计数工具,该工具比通常的AutoCloseable更加灵活。使其泄漏检测器出色的原因在于它提供了不同级别的跟踪(具有不同的开销),并且您可以使用它来捕获堆栈跟踪,以了解泄漏对象的分配和使用位置。