为什么终结器会有“严重的性能惩罚”?

27

《Effective Java》中说:

使用finalizer会造成严重的性能损失。

为什么使用finalizer来销毁一个对象会更慢呢?


1
你可能会喜欢这篇文章,它讲述了finalizers如何使对象再次可达等内容。同时它还展示了为什么在某些情况下组合可以拯救一天(而不是实现继承):http://java.sun.com/developer/technicalArticles/javase/finalization/ - SyntaxT3rr0r
6个回答

23
由于垃圾收集器的工作方式,大多数Java GC使用复制收集器来提高性能。短暂的对象被分配到一个“伊甸园”内存块中,当该对象的生命周期结束时,GC只需要将仍然“存活”的对象复制到更永久的存储空间中,然后可以一次性清除(释放)整个“伊甸园”内存块,这是有效的,因为大多数Java代码将创建成千上万的实例对象(装箱基元、临时数组等),其寿命只有几秒钟。
但是,如果混合了finalizer,GC就不能简单地一次性清除整个代。相反,它需要找出该代中所有需要进行finalize的对象,并将它们排队到实际执行finalizers的线程上。在此期间,GC无法有效地完成对象的清理。因此,它不得不将它们保持活动状态的时间比应该更长,或者延迟收集其他对象,或两者兼而有之。此外,还要考虑实际执行finalizers的任意等待时间。
所有这些因素都导致了显着的运行时惩罚,这就是为什么通常会使用确定性终结(使用类似于close()方法之类的显式finalizer)的原因。

1
当然,这主要关注代际收集器的问题。其他GC策略存在不同的问题。但它们都归结为GC需要执行额外的工作,包括至少两次通过对象以释放它;一次将其添加到finalize队列中,一次在finalization之后实际释放它。 - Lawrence Dol
我想问一下,几个常用的Java API类是否有终结器来释放操作系统资源?我在想FileOutputStream。因此,某些对象的终结器不太可能延迟不使用终结器的对象的垃圾回收,因为大多数程序都会受到影响。 - Raedwald
1
@Raedwald:没错。例如,你可以通过查看OpenJDK源代码来看到FileOutputStream的实现有一个finalizer。(我找不到任何需要标准库实现使用finalizers的东西,但是在实践中,虽然对象本来符合GC的条件,但仍处于等待finalization的状态,它们只会被提升到下一个较老的代(survivor space或tenured),而finalizer则排队运行。但是实际的内存回收要等到下一次收集下一个较老的代时才会进行。 - Daniel Pryden
假设一个对象同时实现了 close()finalize(),如果我们显式调用 close(),是否也会发生这种开销? - Gerardo Cauich

10

曾经遭遇过一个问题:

在Sun HotSpot JVM中,finalizer会在一个被赋予固定低优先级的线程上运行。在高负载应用程序中,很容易创建出需要执行finalization的对象,但低优先级的finalization线程无法及时处理它们。与此同时, finalization-pending对象所占用的堆空间也无法供其他用途使用。最终,你的应用程序可能会因为所有可用内存都被处于待finalization状态的对象所占用而花费所有时间进行垃圾回收。

当然,这还要加之前述Effective Java中描述的不使用finalizers的其他原因。


2
我刚从桌子上拿起我的Effective Java一书,看看他指的是什么。
如果你阅读第2章第6节,他详细介绍了各种性能问题。
“你无法知道finalizer何时运行,甚至是否运行。因为这些资源可能永远不会被声明,所以你必须使用更少的资源。”
我建议您阅读整个部分-它比我在这里复述的要好得多。

2
如果你仔细阅读finalize()的文档,你会注意到finalizer使得一个对象可以防止被GC回收。 如果没有finalizer存在,对象只需被移除,不需要任何进一步的处理。但是如果有finalizer,需要在之后检查对象是否再次“可见”。 虽然我们并不确切知道当前Java GC是如何实现的(事实上,因为不同的Java实现存在,也存在不同的GC),但是可以假设如果对象有finalizer,则GC需要进行额外的工作。

实际上,该页面还提到JVM以不同的方式处理具有非平凡终结器的对象:https://www.fasterj.com/articles/finalizer2.shtml - Gerardo Cauich

1
我的想法是: Java 是一种垃圾回收语言,它根据自己的内部算法释放内存。每隔一段时间,GC 扫描堆,确定哪些对象不再被引用,并释放内存。 Finalizer 中断了这个过程,强制在 GC 循环之外释放内存,可能导致效率低下。 我认为最佳实践是仅在绝对必要时使用 finalizer,例如释放文件句柄或关闭数据库连接,这应该是确定性的。

1
它真的是强制性的,还是仅仅是建议性的? - corsiKa
大多数情况下是正确的,但终结器不会在 GC 循环之外导致内存释放。相反,如果 GC 确定对象需要被终止,它会“复活”该对象,并保持对象不被收集,直到终结器执行完毕。但这可能需要一段时间,因为(如果我没记错的话)终结器直到下次老年代被收集时才会运行。 - Daniel Pryden
2
我认为最佳实践是只在绝对必要的情况下使用finalizers,比如释放文件句柄或关闭数据库连接。需要注意的是,这正是finalizers不适用的地方,因为finalizer可能会运行得非常晚,甚至根本不运行。 - sleske

0
我能想到的一个原因是,如果你的资源都是Java对象而不是本地代码,那么显式内存清理就是不必要的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接