重写Object.finalize()真的不好吗?


34

反对重写的两个主要论点Object.finalize()是:

  1. 您不必决定何时调用它。

  2. 它可能根本不会被调用。

如果我正确理解了这一点,我认为这些理由不足以令人讨厌Object.finalize()

  1. 由VM实施和GC决定何时分配对象的正确时间,而不是开发人员。为什么决定何时Object.finalize()打电话很重要?

  2. 通常,如果我错了,请纠正我,唯一的一次Object.finalize()调用是在GC有机会运行之前终止应用程序的时间。但是,无论如何,当应用程序的进程终止时,对象都会被释放。因此Object.finalize()没有被调用,因为它不需要被调用。开发人员为什么要关心?

每次我使用必须手动关闭的对象(例如文件句柄和连接)时,我都会感到非常沮丧。我必须不断检查对象是否具有的实现close(),并且我肯定在过去的某些时候错过了对它的一些调用。为什么不把它放到VM和GC中,而是通过将close()实现放入其中来处置这些对象,这将变得更加简单,安全Object.finalize()呢?


1
还要注意:与Java 1.0时代的许多API一样,的线程语义finalize()有些混乱。如果您实现了它,请确保它相对于同一对象上的所有其他方法是线程安全的。
billc.cn 2015年

2
当您听到有人说终结器不好时,它们并不意味着您拥有终结器的程序将停止运行。他们意味着最终确定的整个想法是毫无用处的。
user253751'7

1
+1这个问题。下面的大多数答案都指出,诸如文件描述符之类的资源是有限的,应该手动收集它们。内存也是如此,因此,如果我们接受内存收集方面的一些延迟,为什么不接受文件描述符和/或其他资源呢?
mbonnin

在最后一段中,您可以将它留给Java来处理关闭之类的事情,例如文件句柄和连接,而您只需花费很少的精力。使用try-with-resources块-在答案和注释中已经提到了其他几次,但是我认为值得在这里提出。可在docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Answers:


45

以我的经验,有一个唯一的原因要覆盖Object.finalize(),但这是一个很好的理由

放置错误日志记录代码,finalize()如果您忘记调用时将通知您close()

静态分析只能捕获琐碎的使用场景中的遗漏,并且另一个答案中提到的编译器警告对事情的处理过于简单,您实际上必须禁用它们才能完成任何不重要的事情。(与我认识或从未听说过的任何其他程序员相比,我启用的警告要多得多,但我没有启用愚蠢的警告。)

最终确定似乎是确保资源不会被处置的一种很好的机制,但是大多数人以完全错误的方式看待它:他们将其视为备用的备用机制,这是“第二次机会”保护措施,可以自动节省资源。一天,通过处置他们遗忘的资源。这是完全错误的。做任何给定事情的方法必须只有一种:要么总是关闭所有内容,要么终结总是关闭所有东西。但是由于最终确定是不可靠的,因此不可能最终确定。

因此,有一个我称为“ 强制性处置”的方案,它规定程序员负责始终显式关闭实现Closeable或的所有内容AutoCloseable。(try-with-resources语句仍然算作显式关闭。)当然,程序员可能会忘记,所以这就是终结的作用,而不是像魔术仙子那样最终使事情正确的方法:如果发现终结,这close()没有被引用,它尝试调用它,正是因为(在数学上可以确定)成群的n00b程序员将依靠它来完成他们懒惰或缺乏思想的工作。因此,通过强制处理,当完成确定close()没有被调用时,它会记录一条鲜红色的错误消息,告诉程序员用大写的大写字母来修复他的东西。

另一个好处是,有传言说“ JVM将忽略一个琐碎的finalize()方法(例如,一个不做任何事情就返回的方法,例如Object类中定义的方法)”,因此通过强制性处置,您可以避免所有终结处理通过像下面这样编码您的方法,整个系统的开销请参阅alip的答案以获取有关此开销有多可怕的信息)finalize()

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

其背后的思想Global.DEBUG是一个static final变量,其值在编译时是已知的,因此,如果为零,false则编译器将针对整个if语句根本不发出任何代码,这将使其成为一个琐碎的(空的)终结器,这反过来意味着您的类将被视为没有终结器。(在C#中,这将使用一个不错的#if DEBUG块来完成,但是我们可以做的是Java,在Java中,我们在代码上付出了明显的简化,却增加了大脑的开销。)

有关强制性处置的更多信息,以及关于在点网中处置资源的更多讨论,请访问:michael.gr:强制性处置与“可处置性”可恶


2
@MikeNakis不要忘记,Closeable被定义为如果第二次调用就什么都不做:docs.oracle.com/javase/7/docs/api/java/io/Closeable.html。我承认有时我的课程关闭两次时有时会记录警告,但从技术上讲,您甚至不应该这样做。从技术上讲,多次在Closable上调用.close()是完全有效的。
Patrick M

1
@usr归结为您是否信任测试或不信任测试。如果您不信任测试,请确定,以防万一,还要 承受最后的确定开销close()。我相信,如果我的测试不值得信任,那么我最好不要将系统投入生产。
Mike Nakis 2015年

3
@Mike,为了使if( Global.DEBUG && ...构造正常工作,因此JVM将忽略该finalize()方法,因为它是微不足道的,Global.DEBUG必须在编译时进行设置(而不是注入等),这样后面将是无效代码。即使超类也是微不足道super.finalize()Global.DEBUG,对if块之外的调用也足以使JVM将其视为不平凡的(至少与HotSpot 1.8 无关,无论其值如何)#finalize()
ALIP

1
@Mike恐怕就是这种情况。我在您链接到文章中使用测试的(稍作修改的版本)对其进行了测试,并且详细的GC输出(以及令人惊讶的糟糕性能)确认对象已复制到幸存者/旧生成空间,并且需要完整的堆GC才能获得摆脱。
2015年

1
经常被忽视的是存在对象收集早于预期的风险,这使释放终结器中的资源成为非常危险的动作。在Java 9之前,确保终结器在使用中时不会关闭资源的唯一方法是在终结器和使用资源的方法中都在对象上进行同步。这就是它在中工作的原因java.io。如果不是这样的线程安全,它会增加由finalize()... 引起的开销
Holger

28

每次我使用必须手动关闭的对象(例如文件句柄和连接)时,我都会感到非常沮丧。[...]为什么不把它放到VM和GC中,通过将close()实现放入其中就可以处理这些对象Object.finalize()呢?

因为文件句柄和连接(即Linux和POSIX系统上的文件描述符)是一种非常稀缺的资源(在某些系统上可能限制为256个,在其他系统上限制为16384;请参阅setrlimit(2))。无法保证GC会被频繁调用(或在正确的时间)以避免耗尽这种有限的资源。而且,如果GC调用不足(或未在正确的时间运行完成),您将达到该限制(可能较低)。

在JVM中,完成终结是“尽力而为”的事情。它可能不会被调用,或者被调用得很晚。特别是,如果您有很多RAM或您的程序没有分配很多对象(或者大多数对象在转发给足够老的对象之前就死了)复制世代GC生成),因此很少会调用该GC,并且最终化操作可能不会经常运行(甚至可能根本不会运行)。

因此close,如果可能的话,文件描述符必须明确。如果您担心泄漏它们,请使用终结处理作为一种额外的措施,而不是主要措施。


7
我会补充说,关闭流到文件或套接字通常会刷新它。离开流打开不必要地增加了数据丢失的风险,如果停电,连接下降(这也是通过网络访问的文件风险)等

2
实际上,尽管允许的文件描述符数量可能很少,但这并不是真正的棘手问题,因为至少可以将其用作GC的信号。真正有问题的是,a)完全不透明,GC上挂有多少非GC管理的资源,以及b)其中许多资源是唯一的,因此其他资源可能会被阻止或拒绝。
Deduplicator 2015年

2
而且,如果您打开文件,则可能会干扰其他人使用它。(Windows 8 XPS查看器,我在看着你!)
Loren Pechtel 2015年

2
“如果您担心泄漏[文件描述符],请使用终结处理作为一种额外的措施,而不是主要措施。” 这句话对我来说听起来很可疑。如果设计得当,您是否真的应该引入冗余代码,将清理工作分散到多个地方?
mucaho 2015年

2
@BasileStarynkevitch您的观点是,在理想的世界中,冗余是不好的,但是在实践中,您无法预见所有相关方面,总比对不起更安全?
mucaho

13

这样看待问题:您只应编写正确的代码(a)正确(否则您的程序肯定是错误的)和(b)必要的(否则您的代码太大,这意味着需要更多的RAM,花更多的时间)没用的东西,需要更多的努力来理解它,花费更多的时间来维护它,等等等等)

现在考虑要在终结器中完成的操作。要么有必要。在那种情况下,您不能将其放入终结器中,因为您不知道是否会调用它。这还不够好。还是没有必要-那么您不应该首先编写它!无论哪种方式,将其放入终结器都是错误的选择。

(请注意,你的名字的例子,如关闭文件流,看起来就好像他们是不是真的有必要,但他们。这只是直到你击中了打开的文件句柄限制你的系统,你不会通知你代码是不正确的。但是该限制是操作系统的功能,因此比JVM的终结器策略更加不可预测,因此,不要浪费文件句柄确实非常重要。)


如果仅编写“必需”的代码,那么是否应该避免所有GUI应用程序中的所有样式?当然,没有必要。这些GUI无需样式即可正常工作,它们看起来会很恐怖。关于终结器,可能有必要做一些事情,但是将其放入终结器中还是可以的,因为终结器将在对象GC期间调用(如果有的话)。如果您需要关闭资源,特别是在准备好进行垃圾回收时,则终结器是最佳选择。程序将终止释放资源或调用终结器。
克鲁

如果我有一个笨重的,创建起来很昂贵的可关闭对象,并且我决定弱引用它,以便可以在不需要时清除它,那么我可以用finalize()它来关闭它,以防万一真正需要释放gc周期的情况内存。否则,我将对象保留在RAM中,而不必每次需要使用时都重新生成并关闭它。当然,在可能的情况下,直到对象被GC为止,打开的资源都不会被释放,但是我可能不需要保证在特定时间释放我的资源。
克鲁

8

不依赖终结器的最大原因之一是,可能试图在终结器中清除的大多数资源非常有限。垃圾收集器只会如此频繁地运行,因为遍历引用来确定是否可以释放某些内容非常昂贵。这意味着您可能需要一段时间才能真正破坏您的对象。例如,如果您有许多对象打开了短期数据库连接,则让终结器清理这些连接可能会耗尽连接池,同时它等待垃圾收集器最终运行并释放完成的连接。然后,由于等待,您最终会积压大量排队请求,从而很快又耗尽了连接池。它'

另外,使用try-with-resources使得完成时关闭“可关闭”对象变得容易。如果您不熟悉此构造,建议您将其检出:https : //docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html


8

除了将其留给终结器以释放资源通常不是一个好主意之外,可终结对象还具有性能开销。

根据Java理论和实践:垃圾收集和性能(Brian Goetz),终结器不是您的朋友

与没有终结器的对象相比,带有终结器的对象(那些具有非平凡的finalize()方法的对象)具有大量开销,因此应谨慎使用。可终结对象的分配速度和收集速度都较慢。在分配时,JVM必须向垃圾收集器注册任何可终结对象,并且(至少在HotSpot JVM实现中)可终结对象必须遵循比大多数其他对象慢的分配路径。同样,可终结对象的收集也较慢。在可以回收可终结对象之前,至少需要两个垃圾回收周期(在最佳情况下),并且垃圾回收器必须做额外的工作才能调用终结器。结果是花费更多的时间分配和收集对象,并给垃圾收集器带来更大的压力,因为无法访问的可终结对象使用的内存保留的时间更长。结合这样的事实,即finalizer不能保证在任何可预测的时间范围内甚至根本无法运行,您可以看到在相对少的情况下,finalizer是正确的工具。


优点。请参阅我的答案,该答案提供了避免的性能开销的方法finalize()
Mike Nakis

7

我(最Object.finalize不希望避免的)避免原因不是对象可能在您期望之后完成,而是可以您期望之前完成。问题不在于,如果Java认为不再可以访问范围,则可以在退出范围之前完成对仍在范围内的对象的终结。

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

看到这个问题更多详细信息,。更有趣的是,只有在热点优化启动后才能做出此决定,这使得调试起来很痛苦。


1
问题是它HasFinalize.stream本身应该是一个单独的可终结对象。也就是说,的终结处理HasFinalize不应终结或尝试清理stream。或者,如果应该,则应使其stream无法访问。
快速


4

我必须不断检查对象是否具有close()的实现,并且我肯定在过去的某些时候错过了对它的几次调用。

在Eclipse中,每当我忘记关闭实现Closeable/的东西时,我都会收到一条警告AutoCloseable。我不确定这是Eclipse还是正式编译器的一部分,但是您可能会考虑使用类似的静态分析工具来帮助您。例如,FindBugs可能可以帮助您检查是否忘记了关闭资源。


1
好主意一提AutoCloseable。使用资源试一试可以轻松地管理资源。这使问题中的几个参数无效。

2

第一个问题:

由VM实施和GC决定何时分配对象的正确时间,而不是开发人员。为什么决定何时Object.finalize()打电话很重要?

好吧,JVM将确定何时适当地回收已为对象分配的存储。您不一定要在该时间执行清理资源的时间finalize()。SO “ finalize()调用Java 8中的强可访问对象”问题对此进行了说明。在那里,一个close()方法已被某个方法调用finalize(),而同一对象从流中读取的尝试仍在等待中。因此,除了众所周知的可能性finalize()称为延迟的可能性之外,还有被称为为时过早的可能性。

您的第二个问题的前提:

通常,如果我错了,请纠正我,唯一的一次Object.finalize()调用是在GC有机会运行之前终止应用程序的时间。

是完全错误的。JVM完全不需要支持终结处理。嗯,这并不是完全错误,因为您仍然可以将其解释为“应用程序在完成定稿之前就被终止了”,并假设您的应用程序将永远终止。

但是请注意,原始声明的“ GC”与术语“最终确定”之间的细微差别。垃圾回收与完成不同。内存管理一旦检测到对象不可访问,就可以简单地回收其空间,如果没有,则它没有特殊的finalize()方法,或者根本不支持终结处理,或者可以将对象排队以便进行终结处理。因此,垃圾回收周期的完成并不意味着执行终结器。这可能在稍后的时间(队列被处理时)发生,或者根本没有发生。

这也是为什么即使在具有终结支持的JVM上,依靠它进行资源清除也是很危险的原因。垃圾回收是内存管理的一部分,因此由内存需求触发。垃圾回收有可能永远不会运行,因为在整个运行期间有足够的内存(嗯,这仍然符合“应用程序在GC有机会运行之前就终止了”的描述)。GC也有可能运行,但是此后,有足够的内存回收,因此终结器队列不被处理。

换句话说,以这种方式管理的本机资源仍然与内存管理无关。虽然可以保证OutOfMemoryError只有在尝试释放内存后才抛出an ,但这不适用于本机资源和终结处理。如果终结处理程序曾经运行过,则由于终结处理队列中充满了可以释放这些资源的对象而导致资源不足而导致文件打开失败的可能性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.