11

我不太明白为什么在 java 和 c# 等语言中有终结器。AFAIK,他们:

  • 不保证运行(在java中)
  • 如果它们确实运行,它们可能会在相关对象成为最终确定的候选对象后运行任意时间
  • 并且(至少在 java 中),它们甚至会在一个类上产生惊人的巨大性能损失。

那么为什么要添加它们呢?我问了一个朋友,他咕哝着“你希望有一切可能的机会来清理数据库连接之类的东西”,但这让我觉得这是一种不好的做法。为什么你应该依赖具有上述属性的东西来做任何事情,甚至作为最后一道防线?尤其是当类似的东西被设计到任何 API 中时,所说的 API 会被嘲笑不复存在。

4

9 回答 9

16

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

在 .NET CLR 中,例如:

  • 不保证运行

如果程序没有被终止,终结器将始终运行。它何时运行并不确定。

  • 如果它们确实运行,它们可能会在相关对象成为最终确定的候选对象后运行任意时间

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

在 .NET 中,这非常非常有用。在 .NET 中将本机的非 .NET 资源包装到 .NET 类中是很常见的。通过实现终结器,您可以保证正确清理本机资源。如果没有这个,用户将被迫调用一个方法来执行清理,这会大大降低垃圾收集器的效率。

确切地知道何时释放您的(本机)资源并不总是那么容易 - 通过实现终结器,您可以保证它们将被正确清理,即使您的类以不太完美的方式使用。

  • 并且(至少在java中),它们甚至会在一个类上产生惊人的巨大性能损失

同样,.NET CLR 的 GC 在这里具有优势。如果你实现了正确的接口IDisposable(这样做的方式是用户定义的清理方法可以调用GC.SuppressFinalize,它会绕过终结器。

这为您提供了两全其美的优势 - 您可以实现终结器和 IDisposable。如果您的用户正确处理您的对象,则终结器没有影响。如果他们不这样做,终结器(最终)会运行并清理您的非托管资源,但您会在运行时遇到(小的)性能损失。

于 2009-12-05T01:29:26.613 回答
10

嗯,你在这里画的画有点过于乐观了。终结器也不能保证在 .NET 中运行。典型的事故是终结器在终结器线程上抛出异常或超时(2 秒)。

当 Microsoft 决定在 SQL Server 中提供 .NET 托管支持时,这是一个问题。重新启动应用程序以解决资源泄漏的应用程序不被认为是可行的解决方法。.NET 2.0 获得了通过从 CriticalFinalizerObject 类派生的关键终结器。此类的终结器必须遵守受约束执行区域 (CER) 的规则,本质上是抑制异常的代码区域。您可以在 CER 中做的事情非常有限。

回到您最初的问题,终结器是释放内存以外的操作系统资源所必需的。垃圾收集器很好地管理内存,但不会做任何事情来释放笔、画笔、文件、套接字、窗口、管道等。当一个对象使用这样的资源时,它必须确保在完成后释放资源它。终结器确保发生这种情况,即使程序忘记这样做。您几乎从不自己编写带有终结器的类,操作资源由框架中的类包装。

.NET 框架还具有一种编程模式,可确保尽早释放此类资源,以便资源在终结器运行之前不会徘徊。所有具有终结器的类也实现 IDisposable.Dispose() 方法,允许您的代码显式释放资源。.NET 程序员经常忘记这一点,但这通常不会导致问题,因为终结器确保它最终会完成。许多 .NET 程序员已经失去了几个小时的睡眠,担心是否所有的 Dispose() 调用都得到了处理,并且论坛上已经启动了大量关于它的线程。Java 的人一定更快乐。


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

其次,终结器线程故障是严重的程序故障。类似于出现 OutOfMemory 异常或绊倒电源线并拔下机器。除了修复代码中的错误或重新布线之外,您无能为力。对 Microsoft 来说,设计关键的终结器很重要,他们不能依赖所有为 SQL Server 编写 .NET 代码的程序员来正确地编写代码。如果你自己摸索终结者,那么就没有这种责任,你会接到客户的电话,而不是微软。

于 2009-12-05T02:17:25.440 回答
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();
        }
    }
 }
于 2009-12-05T01:42:32.110 回答
4

在 java 中,存在终结器以允许清理外部资源(存在于 JVM 之外并且在“父”java 对象存在时不能被垃圾收集的东西)。这一直很少见。例如,如果您正在与某些自定义硬件进行交互。

我认为 java 中的终结器不能保证运行的原因是它们在程序终止时可能没有机会这样做。

在“纯”java 中使用终结器可能会做的一件事是使用它来测试终止条件——例如检查所有连接是否已关闭,如果未关闭则报告错误。您不能保证错误总是会被捕获,但它可能会至少在某些时候被捕获,这足以揭示错误。

大多数 java 代码都不需要终结器。

于 2009-12-05T01:44:32.340 回答
2

它们旨在释放在对对象的所有引用都被破坏之前无法释放的本地资源(例如套接字、打开的文件、设备),这是特定调用者(通常)无法做到的会心。另一种方法是微妙的、无法追踪的资源泄漏......

当然,在许多情况下,作为应用程序作者,您会知道只有一个对 DB 连接的引用(例如);在这种情况下,当你知道你已经完成了它时,终结器不能替代正确地关闭它。

于 2009-12-05T01:42:29.507 回答
0

在 .Net 土地上,运行时无法保证 t 。但他们会跑。

于 2009-12-05T01:31:14.173 回答
0

您指的是 Object.Finalize 吗?

根据 msdn,“在 C# 代码中,Object.Finalize 不能被调用或覆盖”。事实上,他们推荐使用 Dispose 方法,因为它更可控。

于 2009-12-05T01:33:42.627 回答
0

.NET 中的终结器还有一个额外的复杂性。如果类有终结器并且没有得到 Dispose()'d,或者 Dispose() 没有抑制终结器,垃圾收集器会将收集推迟到压缩第 2 代内存(最后一代)之后,所以对象是“排序的”,但不完全是内存泄漏。(是的,它最终会被清理掉,但很可能直到应用程序终止。)

正如其他人所提到的,如果一个对象拥有非托管资源,它应该实现 IDisposable 模式。开发人员应该知道,如果一个对象实现了 IDisposable,那么它的 Dispose() 方法应该总是被调用。C# 提供了一种使用 using 语句自动执行此操作的方法:

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

using 块在块退出时自动调用 Dispose(),甚至通过返回或抛出异常退出。using 语句仅适用于实现 IDisposable 的对象。

并注意另一个混淆点;Dispose() 是一个对象释放资源的机会,但它实际上并不释放 Dispose() 的对象。当没有对它们的活动引用时,.NET 对象可以进行垃圾回收。从技术上讲,从 AppDomain 开始,任何对象引用链都无法访问它们。

于 2009-12-05T02:32:27.017 回答
0

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

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

于 2017-09-10T16:14:01.367 回答