为什么要实现finalize()?


371

我一直在阅读许多关于Java的新手问题finalize(),发现令人困惑的是,没有人真的说过finalize()是清理资源的不可靠方法。我看到有人评论说他们用它来清理Connections,这确实很吓人,因为要保证关闭Connection的唯一方法就是最终实现try(catch)。

我没有在CS上学,但是近十年来我一直在用Java专业编程,而且从未见过有人finalize()在生产系统中实现过程序。这仍然并不意味着它没有用途,或者与我一起工作的人都在正确地做它。

所以我的问题是,有哪些用例可以实现 finalize()无法通过语言中的其他过程或语法更可靠地处理?

请提供特定的方案或您的经验,仅仅重复一本Java教科书,或完成最终的预期用途是不够的,这不是此问题的意图。


HI finalize()方法在此处很好地解释了 howtodoinjava.com/2012/10/31/…–
Sameer Kazi

2
大多数应用程序和库代码将永远不会使用finalize()。但是,平台库代码(例如SocketInputStream,代表呼叫者管理本地资源)这样做是为了最大程度地降低资源泄漏的风险(或使用PhantomReference稍后添加的等效机制,例如)。因此,生态系统需要它们,即使99.9999%的开发人员永远不会编写一个。
布莱恩·格茨

Answers:


231

您可以将其用作保存外部资源(套接字,文件等)的对象的支持。实现close()需要调用的方法和文档。

如果发现尚未完成finalize(),请实施以进行close()处理。也许把东西丢给stderr指出您是在有错误的呼叫者之后进行清理。

在特殊/越野情况下,它提供了额外的安全性。并非每个呼叫者每次都会做正确的try {} finally {}事情。不幸的是,但在大多数环境中都是如此。

我同意几乎不需要它。正如评论者所指出的,它带有GC开销。仅在需要长时间运行的应用程序中使用“皮带和吊带”安全性时才使用。

从Java 9开始,我Object.finalize()已经过时了!他们指出,我们java.lang.ref.Cleanerjava.lang.ref.PhantomReference作为替代品。


44
只需确保finalize()永远不会引发异常,否则垃圾收集器将不会继续清除该对象,并且会发生内存泄漏。
skaffman's

17
不保证可以调用Finalize,因此不要指望它释放资源。
flicken

19
skaffman-我不相信(除了一些错误的JVM实现)。从Object.finalize()javadoc中:如果finalize方法抛出未捕获的异常,则该异常将被忽略,并且该对象的终止终止。
John M

4
您的建议可能会长时间隐藏人们的错误(而不是立即关闭)。我不建议这样做。
kohlerm

64
出于这个确切的原因,我实际上使用了finalize。我继承了代码,它的bug很大,并且倾向于保持数据库连接打开。我们修改了连接以包含有关何时何地创建数据的数据,然后在未正确关闭连接的情况下实施finalize以记录此信息。事实证明,这对于跟踪未关闭的连接很有帮助。
brainimus 2010年

174

finalize()向JVM暗示在未指定的时间执行代码可能会更好。当您希望代码神秘地无法运行时,这很好。

在以下三种情况下,在终结器中做任何重要的事情(基本上是除日志记录之外的其他事情)也很不错:

  • 您想赌博其他定型对象仍将处于程序其余部分认为有效的状态。
  • 您想要向具有终结器的所有类的所有方法中添加大量检查代码,以确保它们在终结后的行为正确。
  • 您想意外地复活已完成的对象,并花费大量时间试图弄清为什么它们不起作用,和/或为什么最终发布它们时未最终确定它们。

如果您认为需要finalize(),有时您真正想要的是幻影引用(在给定的示例中,该幻影引用可以保存对由其Referand使用的连接的硬引用,并在将幻象引用排队后将其关闭)。它也具有它可能神秘地永远不会运行的特性,但是至少它不能在终结的对象上调用方法或复活终结的对象。因此,在您绝对不需要干净地关闭该连接,但您确实想这样做,并且该类的客户端无法或不会调用关闭自己的情况下,这是正确的(实际上很公平-什么' 如果您设计的接口需要在收集之前采取特定的措施 ,那么根本就没有垃圾收集器了吗?这让我们回到了malloc / free的时代。)

其他时候,您需要您认为自己正在设法变得更强大的资源。例如,为什么需要关闭该连接?它最终必须基于系统提供的某种I / O(套接字,文件等),因此,当获得最低资源级别时,为什么不能依靠系统为您关闭系统呢?如果另一端的服务器绝对要求您干净地关闭连接,而不仅仅是断开插座,那么当有人在运行您的代码的计算机的电源电缆上跳闸,或者中间网络断开时,会发生什么?

免责声明:过去我一直在从事JVM实现。我讨厌终结者。


76
支持极端嘲讽正义。
感知

32
这不能回答问题。它只是说出了所有缺点,finalize()而没有给出任何理由,他们真的想使用它。
机械蜗牛

1
@supercat:虚引用(或者是所有参考)Java 2中引入
史蒂夫·杰索普

1
@supercat:抱歉,是的,我指的是“引用” java.lang.ref.Reference及其子类。Java 1确实有变量:-)而且是的,它也有终结器。
史蒂夫·杰索普

2
@SteveJessop:如果基类构造函数要求其他实体代表正在构造的对象更改其状态(一种非常常见的模式),但是派生类字段初始化器抛出异常,则部分构造的对象应立即清除它本身之后(即通知那些外部实体不再需要它们的服务),但是Java和.NET都没有提供任何甚至远程清除的模式,通过这种模式可以发生这种情况。确实,他们似乎竭尽全力以致于难以引用需要清除的内容才能到达清除代码。
超级猫

58

一个简单的规则:永远不要使用终结器。

仅对象具有终结器(无论执行什么代码)的事实就足以导致垃圾回收的相当大的开销。

摘自Brian Goetz 的文章

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


46

我在生产代码中使用finalize的唯一一次是执行检查,以清除给定对象的资源,如果没有,则记录一条非常明确的消息。它实际上并没有尝试自己做,如果做得不好,它只会大声喊叫。原来是很有用的。


35

自1998年以来,我一直从事Java专业工作,但从未实现过finalize()。不止一次。


那我该如何销毁公共类对象呢?这导致了ADF中的问题。基本上是假设要重新加载页面(从API中获取新数据),但是它使用的是旧对象并显示结果
InfantPro'Aravind'16

2
@ InfantPro'Aravind'调用finalize不会删除该对象。这是您实现的一种方法,JVM可以在垃圾回收对象时以及何时垃圾回收对象。
Paul Tomblin

@PaulTomblin,感谢您提供的详细信息。有刷新ADF页面的建议吗?
InfantPro'Aravind'16

@ InfantPro'Aravind'不知道,从未使用过ADF。
Paul Tomblin

28

可接受的答案是好的,我只是想补充一点,现在有一种方法可以使finalize功能完全不实际使用。

查看“参考”类。弱引用,幻像引用和软引用。

您可以使用它们保留对所有对象的引用,但是该引用ALONE不会停止GC。整洁的事情是,您可以在删除该方法时让它调用一个方法,并且可以保证该方法被调用。

至于finalize:我曾经使用finalize来了解正在释放哪些对象。您可以使用静态函数,引用计数等进行一些巧妙的游戏-但这仅用于分析,但请注意类似这样的代码(不仅在完成过程中,而且您很可能会看到它):

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

这表明有人不知道自己在做什么。几乎不需要这样的“清理”。类经过GC处理后,将自动完成。

如果在finalize中找到这样的代码,则可以确保编写该代码的人感到困惑。

如果在其他地方,则可能是代码是对不良模型的有效补丁(类保留了很长时间,并且由于某种原因,它引用的内容必须在对象进行GC处理之前手动释放)。通常,这是因为有人忘记删除一个监听器或其他东西,并且无法弄清楚为什么不对他们的对象进行GC处理,因此他们只是删除了它所指的内容,耸了耸肩然后走开了。

永远不要使用它来清理“更快”的东西。


3
我认为,幻影引用是一种跟踪释放哪些对象的更好方法。
Bill Michell

2
试图提供我所听过的有关“弱引用”的最佳解释。
sscarduzio

很抱歉,我花了这么多年阅读这篇文章,但这确实是对答案的很好补充。
Spencer Kormos 2014年

27

我不确定您能做什么,但是...

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

因此,我猜太阳在某些情况下(他们认为)应该使用它。


12
我想这也是“ Sun开发人员会永远正确吗”的问题?
CodeReaper 2012年

13
但是,其中有多少用途只是在实施或测试支持finalize而不是实际使用?
机械蜗牛

我使用Windows。在Windows中您将如何做同样的事情?
gparyani

2
@damryfbfnetsi:尝试安装ACK(beyondgrep.com通过的方式)chocolatey.org/packages/ack。或使用中的简单“在文件中查找”功能$FAVORITE_IDE
Brian Cline 2014年

21
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
由于某种原因,此代码使我想到了僵尸电影。您GC了您的对象,然后它再次站起来...
steveha 2012年

以上是好的代码。我想知道如何再次使该对象重新可达。
lwpro2

15
不,上面是错误的代码。这是为什么finalize不好的一个例子。
托尼

请记住,调用System.gc();并不能保证垃圾收集器会真正运行。这是GC的完全酌情决定权,以清理堆(也许仍然有足够的可用堆,也许碎片不多,...)。因此,这只是程序员的一个卑微的要求,“请,您能听到我的声音并跑吗?” 而不是“按我说的立即运行!”命令。很抱歉使用舌头单词,但它确实说明了JVM的GC的工作方式。
罗兰

10

finalize()有助于捕获资源泄漏。如果应关闭资源但不关闭资源,则将其未关闭的事实写入日志文件,然后将其关闭。这样,您可以消除资源泄漏,并给自己提供一种方法来知道它已发生,因此您可以对其进行修复。

从1.0 alpha 3(1995)开始,我就开始使用Java进行编程,但是我还没有重写finalize。


6

您不应该依赖finalize()为您清理资源。如果是的话,finalize()直到类被垃圾回收后才会运行。使用完资源后,显式释放资源会更好。


5

为了突出上述答案中的一点:终结器将在单独的GC线程上执行。我听说过一个主要的Sun演示,其中开发人员在某些终结器上添加了一些睡眠,并故意将其他花哨的3D演示放到了膝盖上。

最好避免,除非可能有test-env诊断程序。

埃克尔(Eckel)的《 Java思维》(The Thinking in Java)中对此有很好的论述。


4

嗯,我曾经用它来清理没有返回到现有池的对象。

他们经过了很多次,因此无法确定何时可以安全地将其送回游泳池。问题在于,它在垃圾回收期间造成了巨大的损失,这远远大于池化对象所节省的成本。在我删除整个池,使所有内容变得动态并完成之后,它已经投入生产了大约一个月。


4

小心自己在 finalize()。尤其是在将其用于调用close()以确保资源被清理之类的情况下。我们遇到了将JNI库链接到正在运行的Java代码的几种情况,并且在任何情况下使用finalize()调用JNI方法的情况,都会导致Java堆损坏。损坏不是由底层JNI代码本身引起的,所有内存跟踪在本机库中都很好。完全是事实,我们完全从finalize()调用JNI方法。

这是与仍在广泛使用的JDK 1.5一起使用的。

我们要等到很久以后才发现出问题了,但最终的罪魁祸首始终是利用JNI调用的finalize()方法。


3

在编写将由其他开发人员使用的代码时,需要调用某种“清理”方法来释放资源。有时那些其他开发人员忘记调用您的清理(或关闭或销毁,或其他方法)。为了避免可能的资源泄漏,您可以签入finalize方法以确保已调用该方法,如果不是,则可以自己调用它。

许多数据库驱动程序在自己的Statement and Connection实现中执行此操作,以为忘记忘记关闭它们的开发人员提供一点安全。


2

编辑:好的,它确实不起作用。我实现了它,并认为有时候失败了对我来说还可以,但是它甚至没有一次调用finalize方法。

我不是专业程序员,但是在我的程序中有一个案例,我认为这是使用finalize()的一个很好的例子,这是一个缓存,它在内容销毁之前将其内容写入磁盘。因为没有必要每次销毁都执行它,所以它只会加速我的程序,我希望它没有做错什么。

@Override
public void finalize()
{
    try {saveCache();} catch (Exception e)  {e.printStackTrace();}
}

public void saveCache() throws FileNotFoundException, IOException
{
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp/cache.tmp"));
    out.writeObject(cache);
}

我认为缓存(您将刷新到磁盘的类型)是一个很好的例子。即使没有完成,也不会费劲。
foo

2

删除已添加到全局/静态位置(不需要)并且在删除对象时需要删除的内容会很方便。例如:

    私人无效addGlobalClickListener(){
        weakAwtEventListener = new WeakAWTEventListener(this);

        Toolkit.getDefaultToolkit()。addAWTEventListener(weakAwtEventListener,AWTEvent.MOUSE_EVENT_MASK);
    }

    @Override
    受保护的void finalize()抛出Throwable {
        super.finalize();

        if(weakAwtEventListener!= null){
            Toolkit.getDefaultToolkit()。removeAWTEventListener(weakAwtEventListener);
        }
    }

这需要付出额外的努力,因为当侦听器this对构造函数中传递给它的实例具有强烈的引用时,该实例将永远不会变得不可访问,因此finalize(),无论如何都不会清除。另一方面,如果顾名思义,侦听器对该对象的引用很弱,则此构造可能会在不该构造的时候被清理。对象生命周期不是实现应用程序语义的正确工具。
Holger

0

iirc-您可以使用finalize方法作为对昂贵资源实施池化机制的手段-因此它们也不会获得GC。


0

附带说明:

覆盖finalize()的对象将由垃圾回收器专门处理。通常,对象不再在作用域内之后,在收集周期中会立即销毁该对象。但是,可终结对象改为移动到队列中,其中单独的终结线程将耗尽队列并在每个对象上运行finalize()方法。一旦finalize()方法终止,该对象将最终准备在下一个循环中进行垃圾回收。

来源:java-9弃用的finalize()


0

完成处理后,需要关闭资源(文件,套接字,流等)。它们通常具有close()我们通常在语句finally部分中调用的方法try-catch。有时finalize(),很少有开发人员也可以使用IMO,但这不是一种合适的方法,因为无法保证将始终调用finalize。

在Java 7中,我们有try-with-resources语句,可以这样使用:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
  // Processing and other logic here.
} catch (Exception e) {
  // log exception
} finally {
  // Just in case we need to do some stuff here.
}

在上面的示例中,try-with-resource将BufferedReader通过调用close()方法自动关闭资源。如果我们愿意,我们也可以在自己的类中实现Closeable并以类似方式使用它。IMO似乎更简洁易懂。


0

就个人而言,finalize()除了一种罕见的情况外,我几乎从未使用过:我制作了一个自定义的泛型类型集合,并编写了一个自定义finalize()方法来执行以下操作:

public void finalize() throws Throwable {
    super.finalize();
    if (destructiveFinalize) {
        T item;
        for (int i = 0, l = length(); i < l; i++) {
            item = get(i);
            if (item == null) {
                continue;
            }
            if (item instanceof Window) {
                ((Window) get(i)).dispose();
            }
            if (item instanceof CompleteObject) {
                ((CompleteObject) get(i)).finalize();
            }
            set(i, null);
        }
    }
}

CompleteObject是我做的界面,可以让你指定你已经实现了很少实现的Object方法,如#finalize()#hashCode()#clone()

因此,使用姊妹#setDestructivelyFinalizes(boolean)方法,使用我的集合的程序可以(帮助)确保破坏对此集合的引用也破坏对其内容的引用,并处置任何可能无意间使JVM保持活动状态的窗口。我考虑过还要停止任何线程,但是这打开了一个全新的蠕虫罐。


4
呼唤finalize()你的CompleteObject情况就像你在上面的代码做意味着它可能会调用两次,因为JVM可能仍然调用finalize(),一旦这些物品竟然成了无法访问,其由于最终确定的逻辑,可能是之前finalize()你的特殊的收藏方法甚至在同一时间被调用。由于我看不到任何确保线程安全的措施,因此您似乎没有意识到……
Holger

0

接受的答案列出了可以在完成期间关闭资源。

但是,此答案表明,至少在带有JIT编译器的java8中,您遇到了意料之外的问题,有时甚至在完成从对象维护的流中读取之前,也需要调用终结器。

因此,即使在这种情况下,也不建议调用finalize 。


如果必须使操作成为线程安全的,那将是可行的,因为终结器可以由任意线程调用。由于正确实现的线程安全性意味着不仅关闭操作而且使用资源的操作还必须在同一对象或锁上同步,所以使用和关闭操作之间将存在先发生后关系,这也可以防止过早定案。但是,当然,整个使用过程都需要付出高昂的代价……
Holger
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.