为什么要实现finalize()方法?

394
我一直在阅读很多初学Java的人关于finalize()的问题,发现没有人真正明确地指出finalize()是一种不可靠的清理资源的方式。我看到有人评论说他们使用它来清理连接,这真的很令人担忧,因为接近保证连接已关闭的唯一方法是实现try (catch) finally。
我没有接受过计算机科学的教育,但我已经在Java职业编程近十年了,我从未见过有人在生产系统中实现过finalize()。这仍然不意味着它没有用途,也不意味着我与之合作的人一直做得正确。
所以我的问题是,在哪些用例中需要实现finalize(),而其他语言内部的其他处理过程或语法无法更可靠地处理呢?
请提供具体的场景或您的经验,简单重复Java教科书,或者finalize的预期使用是不够的,因为这不是这个问题的意图。

这里非常好地解释了HI finalize()方法:http://howtodoinjava.com/2012/10/31/why-not-to-use-finalize-method-in-java/。 - Sameer Kazi
5
大多数应用程序和库代码都不会使用 finalize() 方法。然而,处理调用者的本地资源的平台库代码(例如 SocketInputStream)会这样做,以尽量减少资源泄漏的风险(或使用后来添加的等效机制,如 PhantomReference)。因此,即使99.9999%的开发人员永远不会编写它们,生态系统仍然需要这些方法。 - Brian Goetz
4
更新:从Java 9开始,finalize已经被弃用。请参阅问题为什么Java 9中的finalize()方法已被弃用? - Basil Bourque
21个回答

244

你可以将它用作持有外部资源(套接字、文件等)的对象的备案。实现一个close()方法并记录需要调用它。

如果检测到close()方法未被调用,则实现finalize()方法来执行close()处理。可能会有一些内容被转储到stderr以指出正在清理错误的调用者。

在异常或错误情况下提供额外的安全性。并不是每个调用者都会每次都做正确的try {} finally {}。这在大多数环境中都是不幸的,但却是真实的。

我同意它很少被使用。正如评论者所指出的,它会带来GC开销。只有在长时间运行的应用程序中需要"双保险"安全性时才使用。

我注意到Java 9已经弃用了Object.finalize() 方法!他们给我们指出了java.lang.ref.Cleanerjava.lang.ref.PhantomReference 作为替代方案。


50
请确保在finalize()方法中不会抛出异常,否则垃圾回收器将停止清理该对象,导致内存泄漏。 - skaffman
17
无法保证一定会调用Finalize方法,因此不能指望该方法释放资源。 - flicken
20
skaffman - 我不相信除了一些有缺陷的JVM实现之外。从Object.finalize() javadoc中可以看到:如果finalize方法抛出未捕获的异常,则该异常将被忽略,并且该对象的终结将终止。 - John M
5
您的提议可能会长时间隐藏人们的错误(没有调用关闭),我不建议这样做。 - kohlerm
70
为了解决这个问题,我实际上使用了 finalize 方法。我继承了这段代码,它非常有 bug 并且倾向于让数据库连接保持打开状态。我们修改了连接以包含关于创建时间和位置的数据,并实现了 finalize 方法来记录这些信息,如果连接没有正确关闭的话。结果这对于追踪未关闭的连接非常有帮助。 - brainimus
显示剩余13条评论

186

finalize()是给JVM的一个提示,表明在某个不确定的时间执行代码可能会很好。当你想让代码神秘地无法运行时这是有用的。

在finalizers中进行重要操作(基本上除了记录日志之外的任何操作)也适用于三种情况:

  • 您希望赌其他已完成终结的对象仍处于程序其余部分认为有效的状态。
  • 您希望向所有具有终结器的类的所有方法添加大量检查代码,以确保它们在终结后的行为正确。
  • 您希望意外地重新使用已完成终止的对象,并花费大量时间尝试弄清楚它们为什么不起作用和/或在最终被释放时为什么不被终结。

如果您认为需要使用 finalize(),有时实际上您需要的是一个虚引用(在给出的示例中,它可能持有对其引用的连接的硬引用,并在虚引用排队后关闭该连接)。它还具有可能神秘地永远不会运行的属性,但至少它不能在已完成终止的对象上调用方法或使其重新复活。因此,它非常适合那些您不绝对需要干净地关闭该连接,但您相当希望这样做的情况,而且您类的客户端无法或不愿调用close()(这其实是可以理解的 - 如果您设计了需要在收集之前执行特定操作的接口,那么拥有垃圾回收器的意义何在?这只会使我们回到malloc / free的时代。)

其他时候,您需要管理的资源更加健壮。例如,为什么需要关闭该连接?它最终必须基于系统提供的某种I/O(套接字、文件等),因此当最低级资源被gced时,您为什么不能依赖系统来为您关闭它?如果另一端的服务器绝对要求您干净地关闭连接而不只是断开套接字,那么当某人绊倒正在运行代码的机器的电源电缆或中间的网络中断时会发生什么?

免责声明:我曾经在 JVM 实现上工作过,我讨厌 finalizers。


82
顶起以维护极端讽刺的公正。 - Perception
32
这并没有回答这个问题,只是列举了finalize()的所有缺点,没有提供任何人真正想要使用它的原因。 - Mechanical snail
1
@supercat:幽灵引用(以及所有引用)是在Java 2中引入的。 - Steve Jessop
1
@supercat:抱歉,是的,我指的是java.lang.ref.Reference及其子类的“引用”。Java 1确实有变量 :-) 是的,它也有finalizers。 - Steve Jessop
2
@SteveJessop:如果一个基类构造函数要求其他实体代表正在构建的对象改变它们的状态(这是一种非常常见的模式),但派生类字段初始化程序抛出异常,则部分构建的对象应立即清理自身(即通知那些外部实体他们不再需要服务),但是Java和.NET都没有提供任何可以通过该方式进行清理的甚至近乎完美的模式。事实上,它们似乎费尽心思使需要清理的东西的引用达到清理代码很难。 - supercat
显示剩余5条评论

56

一个简单的规则:永远不要使用finalizers。

仅仅因为一个对象有一个finalizer(无论它执行什么代码),就足以为垃圾回收造成相当大的开销。

来自Brian Goetz的文章

具有finalizer(即具有非平凡finalize()方法的对象)与没有finalizer的对象相比,具有显着的开销,并且应该谨慎使用。Finalizeable对象在分配和收集时都比较慢。在分配时,JVM必须向垃圾回收器注册任何可终结对象,并且(至少在HotSpot JVM实现中)可终结对象必须遵循比大多数其他对象更慢的分配路径。同样,可终结对象的收集速度也较慢。在最佳情况下,需要至少两个垃圾回收周期才能回收可终结对象,并且垃圾回收器必须做额外的工作来调用finalizer。结果是花费更多时间分配和收集对象,并对垃圾回收器施加更大压力,因为不可达的可终结对象占用的内存会保留更长时间。再加上finalizer不能保证在任何可预测的时间范围内运行,甚至可能根本不运行,你就会发现只有相对很少的情况适合使用finalization。


50

我在生产代码中仅使用finalize的一次情况是为了实现检查给定对象的资源是否已被清除,如果没有清除,则记录一个非常显眼的消息。 它实际上并没有尝试自己清理资源,只是在未正确完成时大声喊叫。 结果证明它非常有用。


这是唯一有效的用法。您可以使用almson-refcount来实现此目的,因为正确高效地实现它很棘手。 - Aleksandr Dubinsky

39

我从1998年开始职业使用Java,但我从来没有实现过finalize()方法。就一次都没有。


我该如何销毁公共类对象呢?这在ADF中引起了问题。基本上,它应该重新加载页面(从API获取新鲜数据),但它却使用旧对象并显示结果。 - Rookie Programmer Aravind
2
@InfantPro'Aravind' 调用finalize方法并不会移除对象。它是一种你实现的方法,JVM可以选择在垃圾回收你的对象时调用它。 - Paul Tomblin
@PaulTomblin,好的,谢谢您提供这些细节。有关ADF页面刷新,您有什么建议吗? - Rookie Programmer Aravind
@InfantPro'Aravind' 没有头绪,从未使用过ADF。 - Paul Tomblin

31

接受的答案很好,我只想补充一点,现在有一种方法可以实现finalize的功能,而不必实际上使用它。

看看“参考”类。弱引用、幽灵引用和软引用。

可以使用它们来保持对所有对象的引用,但仅此引用不会阻止GC。这个巧妙之处在于,可以在将要删除引用时调用一个方法,并且可以保证调用该方法。

至于finalize: 我曾经使用finalize来了解哪些对象被释放了。您可以玩一些有趣的游戏,如静态、引用计数等,但仅仅是为了分析,但要小心像这样的代码(不仅仅是在finalize中,但这是您最可能看到它的地方):

public void finalize() {
  ref1 = null;
  ref2 = null;
  othercrap = null;
}

这是某些人不知道自己在做什么的迹象。“清理”像这样的操作几乎从来不需要。当类被垃圾回收时,这将自动完成。

如果您在finalize中发现了这样的代码,那么可以保证编写此代码的人很困惑。

如果它出现在其他地方,可能是该代码对错误模型的有效修补(一个类存在很长时间,由于某种原因需要手动释放其引用的东西,然后才能进行GC)。通常是因为有人忘记删除监听器或其他东西,并且无法弄清楚为什么它们的对象没有被GC,所以他们只是删除它们引用的内容,耸耸肩走开。

它永远不应该用于“更快地”清理东西。


3
幽灵引用是更好的跟踪哪些对象被释放的方式,我认为。 - Bill Michell
2
为提供我听过的最好的“弱引用”解释而点赞。 - sscarduzio
很抱歉我花了这么多年才读到这篇内容,但是它确实是答案中不错的补充。 - Spencer Kormos
这个模式现在在JDK中被实现为Cleaner。它更高效,但与finalization并没有真正的区别。 - Aleksandr Dubinsky

28

我不确定你能从中得出什么,但是...

itsadok@laptop ~/jdk1.6.0_02/src/
$ find . -name "*.java" | xargs grep "void finalize()" | wc -l
41

我猜太阳公司找到了一些(他们认为)应该使用它的情况。


12
我猜这也是一个“Sun(公司)的开发人员总是正确吗?”的问题。 - CodeReaper
13
有多少使用案例只是为了实现或测试 finalize 的支持,而不是真正使用它? - Mechanical snail
我使用Windows。你会如何在Windows中完成同样的事情? - gparyani
2
尝试通过http://chocolatey.org/packages/ack安装ack(http://beyondgrep.com/)。或者在您最喜欢的IDE中使用简单的“查找文件”功能。 - Brian Cline
我在JDK 17中尝试了这个命令,结果是零 :) - Venkateswara Rao

20
class MyObject {
    Test main;

    public MyObject(Test t) {    
        main = t; 
    }

    protected void finalize() {
        main.ref = this; // let instance become reachable again
        System.out.println("This is finalize"); //test finalize run only once
    }
}

class Test {
    MyObject ref;

    public static void main(String[] args) {
        Test test = new Test();
        test.ref = new MyObject(test);
        test.ref = null; //MyObject become unreachable,finalize will be invoked
        System.gc(); 
        if (test.ref != null) System.out.println("MyObject still alive!");  
    }
}

抱歉,您需要提供需要翻译的具体内容才能进行翻译。
This is finalize

MyObject still alive!

因此,您可以在finalize方法中使无法访问的实例可访问。

21
出于某种原因,这段代码让我想起了僵尸电影。你将对象进行垃圾回收,然后它又重新站起来了… - steveha
以上是好的代码。我刚才在想如何使对象再次可达。 - Jackie
17
不,上面的代码是错误的。这是为什么finalize方法不好的一个例子。 - Tony
1
请记住,调用 System.gc(); 并不能保证垃圾回收器会真正运行。清理堆的工作完全由GC自行决定(可能还有足够的空闲堆,或者碎片化不是很严重...)。因此,这只是程序员的一个谦恭请求:“请听我一声叹息并运行”,而不是一个命令:“按照我的话现在运行!”抱歉使用了语言学上的词汇,但它确切地说明了JVM的GC如何工作。 - Roland

11

finalize() 可以用来捕获资源泄漏。如果资源应该被关闭但未被关闭,则将其未关闭的事实写入日志文件并关闭它。这样,您可以消除资源泄漏并提供一种方法知道它已发生,从而可以修复它。

自从1.0 alpha 3(1995年)以来,我一直在使用Java编程,但我尚未为任何内容覆盖 finalize() 方法...


6

不应该依赖finalize()来帮你清理资源。finalize()只有在类被垃圾回收时才会运行(如果能运行的话)。最好在使用完资源后显式地释放它们。


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