为什么Java和C#中会有终结器?

11

我不太理解为什么像Java和C#这样的语言中会有终结器。据我所知,它们:

  • 无法保证运行(在Java中)
  • 如果它们确实运行,可能会在相关对象成为终结候选对象之后的任意时间运行
  • 而且(至少在Java中),为了将其附加到类上,它们会产生非常大的性能损失。

那么它们到底是为什么而添加进来的呢?我问了一个朋友,他咕哝着说“您希望拥有每一个清除诸如DB连接等东西的可能机会”,但我认为这是一种不好的做法。为什么您要依赖具有上述特性的东西来进行任何事情,甚至是最后的防线?特别是当如果任何API中都设计了类似的东西,该API就会被嘲笑而不存在。


似乎 try { .. } finally { .. } 块在 Java 中负责实现类似析构函数的行为。 - PP.
try {...} finally {...} 可以用于清理仅在代码段中使用的本地资源。finalize() 用于清理在对象级别上使用的资源。 - Nate
9个回答

16

它们在某些情况下非常有用。

例如,在.NET CLR中:

  • 不能保证运行

如果程序没有被杀死,那么最终器最终一定会运行。只是不确定何时运行。

  • 如果它们运行了,在相关对象成为终结候选对象之后可能会运行任意长的时间

这是真的,但它们仍然会运行。

在.NET中,这非常有用。在.NET中,将本机非.NET资源封装到.NET类中非常常见。通过实现最终器,您可以确保本机资源得到正确清理。否则,用户将被迫调用一个执行清理的方法,这会大大降低垃圾收集器的效率。

并不总是容易知道何时释放您的(本机)资源-通过实现一个最终器,您可以确保它们将得到正确清理,即使您的类以不完美的方式使用。

  • (至少在Java中),甚至将其附加到类上都会产生巨大的性能损失

同样,在这里,.NET CLR的GC具有优势。如果实现了正确的接口(IDisposable),并且开发人员正确实现了它,就可以防止最终化的昂贵部分发生。这是通过用户定义的清理方法调用GC.SuppressFinalize实现的,该方法将绕过最终器。

这样做可以兼顾两全 - 你既能实现终结器(finalizer),也能使用IDisposable接口。如果用户正确地释放了对象,终结器不会产生影响。如果他们没有,终结器最终会运行并清理未托管的资源,但是它的运行会导致(小)性能损失。


好的,我猜我的问题更偏向于Java :) 我忘记了在.NET框架中,终结器是被保证会运行的。 - RCIX
如果不能保证finalizers会运行,为什么他们还要添加它们呢? - ChaosPandion
2
实际上,它们最终会运行。这更多是Java中线程优先级和Java GC架构的问题。不幸的是,在实际应用中,它们并不总是被执行。从技术上讲,即使在Java中,它们也应该最终运行。 - Reed Copsey
1
你可以查看 finalize() 的文档 - 从技术上讲,它们应该在对象成为 GC 候选之后的某个时刻始终(最终)运行:http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Object.html#finalize() - Reed Copsey
2
一个发生死锁的终结器将会阻止所有剩余的终结器运行,因为它们是按顺序运行的。 - Brian Rasmussen
显示剩余2条评论

10
您所看到的描述可能过于乐观了,.NET中的Finalizer并没有得到保证能够运行。通常会发生的问题是Finalizer抛出异常或者Finalizer线程超时(2秒)。
这在微软决定在SQL Server中提供.NET托管支持时成为了一个问题。这种应用程序不认为重新启动应用程序以解决资源泄漏是可行的解决方法。.NET 2.0引入了关键的Finalizer,通过从CriticalFinalizerObject类派生来实现。这样的类的Finalizer必须遵守受限执行区域(CERs)的规则,这本质上是一段禁止异常抛出的代码区域。在CER中可以做的事情非常有限。
回到您最初的问题,Finalizer对于释放操作系统资源(除了内存之外的其他资源)是必要的。垃圾收集器对于内存管理得非常好,但是对于释放笔、画刷、文件、套接字、窗口、管道等资源却无能为力。当一个对象使用这样的资源时,必须确保在完成后释放该资源。Finalizer确保在程序忘记释放时也能够释放资源。您几乎不需要自己编写带有Finalizer的类,因为框架中已经包装了操作资源的类。
.NET框架还有一种编程模式,可以确保及早释放这样的资源,以便资源不会在Finalizer运行之前闲置。所有具有Finalizer的类也实现了IDisposable.Dispose()方法,允许您的代码显式地释放资源。这经常被.NET程序员忘记,但通常不会引起问题,因为Finalizer能够确保最终释放。许多.NET程序员因此担心没有处理所有Dispose()调用而失去了很多睡眠时间,并在论坛上开启了大量线程进行讨论。Java的开发者可能会更加开心。

回应您的评论:在终结器线程中出现异常和超时问题是您不必担心的。首先,如果您发现自己正在编写终结器,请深呼吸,问问自己是否正确使用了该类。终结器是为框架类设计的,您应该使用这样一个类来使用操作资源,您将免费获得该类的终结器。一直到SafeHandle类,它们都有一个关键终结器。

其次,终结器线程失败是程序的严重故障。就像获得OutOfMemory异常或绊倒电源线并将机器拔掉一样。除了修复代码中的错误或重新路由电缆外,您无法做任何事情。对于Microsoft来说,设计关键终结器非常重要,他们不能指望所有为SQL Server编写.NET代码的程序员都能正确地编写代码。如果您自己搞砸了终结器,那么就没有责任可以追究,客户会打电话找您,而不是找Microsoft。


但我的问题是,既然finalizers不能保证运行(在Java中,并且根据您的说法C#),那么它们如何可靠地确保任何事情? - RCIX
我在下面的线条下扩展了我的答案。 - Hans Passant

4
如果您阅读finalize()的JavaDoc,它会说:“当垃圾回收确定没有对该对象的引用时,由垃圾收集器在对象上调用。子类重写finalize方法以处理系统资源或执行其他清理。”

http://java.sun.com/javase/6/docs/api/java/lang/Object.html#finalize

所以这就是“为什么”。我想你可以争论他们的实现是否有效。 我发现finalize()最好的用途是检测释放池资源的错误。大多数泄漏的对象最终都会被垃圾回收,您可以生成调试信息。
class MyResource {
    private Throwable allocatorStack;

    public MyResource() {
        allocatorStack = new RuntimeException("trace to allocator");
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("Bug!");
            allocatorStack.printStackTrace();
        } finally {
            super.finalize();
        }
    }
 }

1
在终结器中打印调试信息可能是你能做的唯一有用的事情... - sleske

4
在Java中,finalizer存在的目的是为了清理外部资源(即存在于JVM之外且不能在“父”Java对象被垃圾回收时进行垃圾回收的资源)。这种情况非常罕见,例如当您与某些自定义硬件进行交互时可能会用到它。
我认为Java中finalizer没有被保证运行的原因是它们可能没有机会在程序终止时运行。
在“纯”Java中,您可以使用finalizer来测试终止条件,例如检查所有连接是否关闭并在未关闭时报告错误。虽然无法保证每次都能捕获到错误,但至少有一些时间可以发现错误。
大多数Java代码不需要使用finalizer。

2

这些终结器的作用是释放本地资源(例如套接字、打开的文件、设备),这些资源在所有引用该对象的引用都被断开之前无法释放,一般情况下特定的调用方无法知道这种情况。否则,将会出现难以追踪的资源泄漏问题...

当然,在许多情况下,作为应用程序作者,您可能会知道只有一个对数据库连接的引用(例如);在这种情况下,终结器不能替代在完成使用后适当关闭它。


0

在.NET中,终结器(finalizer)还存在一个额外的复杂性。如果类具有终结器并且没有被Dispose()处理,或者Dispose()没有抑制终结器,垃圾回收器将推迟收集直到紧凑化第2代内存(最后一代),因此该对象“有点像”但不完全是内存泄漏。(是的,它最终会被清理干净,但很可能要等到应用程序退出之前才能清理)

正如其他人提到的,如果一个对象拥有非托管资源,它应该实现IDisposable模式。开发人员应该意识到,如果一个对象实现了IDisposable,则它的Dispose()方法应该总是被调用。C#提供了一种使用using语句自动完成这个过程的方式:

using (myDataContext myDC = new myDataContext())
{
    // code using the data context here
}

using 块会在块退出时自动调用 Dispose(),即使是通过 return 或抛出异常退出。using 语句仅适用于实现 IDisposable 接口的对象。

还要注意另一个混淆点;Dispose() 是释放资源的机会,但它实际上并没有释放已经被 Dispose() 的对象。只有当没有活动引用指向 .NET 对象时,它们才能够进行垃圾回收。从技术上讲,它们不能被任何起始于 AppDomain 的对象引用链所访问。


在 .net 中另一个复杂的问题是,在系统发现没有强根引用指向对象 Foo(将其添加到应尽快完成的对象列表中)和 Foo 的终结器实际运行之间,可能会有其他对象创建强根引用。如果发生这种情况,Foo 将成为一个“定时炸弹”,Foo 或使用它的任何类都无法防止它。 - supercat

0
在 .Net 领域中,它们的运行时间何时并不保证。但它们最终会被执行。

0
你是在指 Object.Finalize 吗?
根据 MSDN,"在 C# 代码中,无法调用或重写 Object.Finalize"。实际上,他们建议使用 Dispose 方法,因为它更可控。

我在谈论C++程序员通常称之为析构函数或~SomeClass()的内容。 - RCIX

0

C++中destructor()的等价物是Java中的finalizer()

它们在对象的生命周期即将结束时被调用。


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