既然 .NET 有一个垃圾收集器,为啥我们需要终结器/析构器/处置模式?

Posted

技术标签:

【中文标题】既然 .NET 有一个垃圾收集器,为啥我们需要终结器/析构器/处置模式?【英文标题】:Since .NET has a garbage collector why do we need finalizers/destructors/dispose-pattern?既然 .NET 有一个垃圾收集器,为什么我们需要终结器/析构器/处置模式? 【发布时间】:2010-09-24 19:28:36 【问题描述】:

如果我理解正确,.net 运行时将始终在我之后清理。因此,如果我创建新对象并停止在代码中引用它们,运行时将清理这些对象并释放它们占用的内存。

既然是这种情况,为什么有些对象需要有析构函数或处置方法呢?当它们不再被引用时,运行时不会在它们之后清理吗?

【问题讨论】:

【参考方案1】:

某些对象可能需要清理低级项目。比如需要关闭的硬件等。

【讨论】:

【参考方案2】:

需要终结器来保证将稀缺资源释放回系统,例如文件句柄、套接字、内核对象等。由于终结器总是在对象生命周期结束时运行,因此它是释放这些句柄的指定位置。

Dispose 模式用于提供资源的确定性销毁。由于 .net 运行时垃圾收集器是非确定性的(这意味着您永远无法确定运行时何时会收集旧对象并调用它们的终结器),因此需要一种方法来确保系统资源的确定性释放。因此,当您正确实施Dispose 模式时,您会提供资源的确定性释放,并且在消费者粗心且不处置对象的情况下,终结器将清理对象。

为什么需要Dispose 的一个简单示例可能是快速而肮脏的日志方法:

public void Log(string line)

    var sw = new StreamWriter(File.Open(
        "LogFile.log", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None));

    sw.WriteLine(line);

    // Since we don't close the stream the FileStream finalizer will do that for 
    // us but we don't know when that will be and until then the file is locked.

在上面的示例中,文件将保持锁定状态,直到垃圾收集器调用 StreamWriter 对象上的终结器。这带来了一个问题,因为在此期间,可能会再次调用该方法来写入日志,但这一次它将失败,因为文件仍处于锁定状态。

正确的方法是在使用完对象后处理它:

public void Log(string line)

    using (var sw = new StreamWriter(File.Open(
        "LogFile.log", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))) 

        sw.WriteLine(line);
    

    // Since we use the using block (which conveniently calls Dispose() for us)
    // the file well be closed at this point.

顺便说一句,技术上终结器和析构器的意思是一样的;我更喜欢将 c# 析构函数称为“终结器”,否则它们往往会将人们与 C++ 析构函数混淆,而 C++ 析构函数与 C# 不同,是确定性的。

【讨论】:

IMO 这是最好的答案。其中最重要的部分——以及我们使用一次性语法的原因——是提供稀缺资源的确定性释放。很棒的帖子。 很好的答案,尽管终结器不会在对象生命周期结束时自动运行。否则我们不需要一次性模式。当 GC 确定需要运行它们时(谁知道何时),它们会被 GC 调用。 仅作记录。终结器不能保证运行。它们由专用线程按顺序执行,因此如果终结器进入死锁,则不会运行其他终结器(并且内存会泄漏)。显然终结器不应该阻塞,但我只是说有一些警告。 这可能就是为什么有传言说框架可能开始使用线程池来执行终结器的原因。 Eric Lippert 最近写了一篇关于析构函数/终结函数blogs.msdn.com/ericlippert/archive/2010/01/21/… 的博客【参考方案3】:

需要析构函数和处置方法的对象正在使用非托管资源。所以垃圾收集器无法清理那些资源,你必须自己做。

查看 IDisposable 的 MSDN 文档; http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

该示例使用非托管处理程序 - IntPr。

【讨论】:

GC可以清理资源,你只是不知道什么时候。 GC 可以通常清理资源,但并非总是如此。例如,在 System.DirectoryServices.SearchResultCollection 的 MSDN 文档中:“由于实现限制,SearchResultCollection 类在垃圾回收时无法释放其所有非托管资源”【参考方案4】:

主要针对非托管代码,以及与非托管代码的交互。 “纯”托管代码永远不需要终结器。另一方面,Disposable 只是一种方便的模式,可以在您使用完某物后强制释放它。

【讨论】:

【参考方案5】:

垃圾收集器只会在系统没有内存压力的情况下运行,除非它真的需要释放一些内存。这意味着,您永远无法确定 GC 何时运行。

现在,假设您是一个数据库连接。如果你让 GC 在你之后清理,你可能会连接到数据库的时间比需要的长得多,从而导致奇怪的负载情况。在这种情况下,您需要实现 IDisposable,以便用户可以调用 Dispose() 或使用 using() 来真正确保尽快关闭连接,而不必依赖可能会在更晚运行的 GC。

通常,IDisposable 在任何使用非托管资源的类上实现。

【讨论】:

INCORRECT => "垃圾收集器只有在系统没有内存压力的情况下才会运行,除非它真的需要释放一些内存。"实际上,这种说法是不正确的。 GC 在 3 种情况下运行(其中只有一种是确定性的):1)当请求内存分配并且已超过该对象生成的当前段大小时,2)系统处于内存压力(OS)下,3)正在卸载 AppDomain INCORRECT => "通常,IDisposable 在任何使用非托管资源的类上实现。"这种说法也是不正确的。 IDisposable 模式应该在类成员实现 IDisposable 的任何时候实现,并且总是在您处理非托管资源时实现【参考方案6】:

.NET 垃圾收集器知道如何在 .NET 运行时处理托管对象。但是 Dispose 模式 (IDisposable) 主要用于应用程序正在使用的非托管对象。

换句话说,.NET 运行时不一定知道如何处理每种类型的设备或处理那里(关闭网络连接、文件句柄、图形设备等),因此使用 IDisposable 提供了一种说法在一个类型中“让我自己实现一些清理”。看到该实现,垃圾收集器可以调用 Dispose() 并确保清理托管堆之外的东西。

【讨论】:

谢谢...通过将“.NET 堆栈/堆外部”更改为“托管堆”来澄清。【参考方案7】:

在少数(非常少)情况下,当不再使用纯托管对象时可能需要执行特定操作,我想不出一个例子,但我有多年来看到了一些合法的用途。但主要原因是清理对象可能正在使用的所有非托管资源。

因此,一般来说,除非您使用非托管资源,否则您不需要使用 Dispose/Finalize 模式。

【讨论】:

【参考方案8】:
    有些东西在你之后垃圾收集器无法清理 即使是它可以清理的东西,您也可以帮助它尽快清理

【讨论】:

【参考方案9】:

因为垃圾收集器无法收集托管环境未分配的内容。因此,任何导致内存分配的非托管 API 调用都需要以老式方式收集。

【讨论】:

【参考方案10】:

真正的原因是因为 .net 垃圾收集并非旨在收集非托管资源,因此这些资源的清理工作仍掌握在开发人员手中。 此外,当对象超出范围时,不会自动调用对象终结器。 GC 在某个未确定的时间调用它们。当它们被调用时,GC 不会立即运行它,它会等待下一轮调用它,从而增加了清理更多的时间,当您的对象持有稀缺的非托管资源(例如文件)时,这不是一件好事或网络连接)。 进入一次性模式,开发人员可以在确定的时间手动释放稀缺资源(调用 yourobject.Dispose() 或 using(...) 语句时)。 请记住,您应该调用 GC.SuppressFinalize(this);在您的 dispose 方法中告诉 GC 该对象是手动处置的,不应最终确定。 我建议你看看 K. Cwalina 和 B. Abrams 的框架设计指南一书。它很好地解释了 Disposable 模式。

祝你好运!

【讨论】:

【参考方案11】:

前面的答案很好,但让我再次强调这里的重点。特别是你说的

如果我理解正确,.net 运行时将始终在我之后清理。

这只是部分正确。事实上,.NET 为一种特定资源提供自动管理:主内存。所有其他资源都需要手动清理。1)

奇怪的是,在几乎所有关于程序资源的讨论中,主存都有特殊的地位。这当然有一个很好的理由——主内存通常是最稀缺的资源。但值得记住的是,还有其他类型的资源也需要管理。


1) 通常尝试的解决方案是将其他资源的生命周期与代码中的内存位置或标识符的生命周期结合起来——因此存在终结器。

【讨论】:

您可以通过指出它是错误的解决方案来改进该脚注!可替代商品和不可替代商品必须区别对待。 Earwicker:我同意你的看法。但是,由于我不知道任何实现可行替代方案的语言,我真的不知道什么会更好。特别是因为每个资源都绑定到一个标识符,并且该标识符与其内存具有相同的生命周期。 C# 的 using 关键字是一个可行的替代方案:当执行离开代码块时,就该释放资源了。对于不可替代的资源,这比将它们的生命周期与释放的内存等可替代的资源联系起来更可取。 @Earwicker:这是我不再同意的地方。 using 有利有弊,但我不确定前者是否胜过后者。当然,这取决于应用程序,但在我编写的几乎每个程序中,非托管资源管理都是至关重要的部分,而 C++ 让我的生活变得更加轻松。 您可能想查看 C++/CLI 以了解析构函数如何完美映射到 IDisposable。我同意 C++/CLI 的支持更完整,因为它会自动传播对成员对象、继承对象等的 Dipose 调用,其中 C# 的使用仅重现 C++ 如何处理堆栈上的对象。【参考方案12】:

简单的解释:

Dispose 旨在确定性处置非内存资源,尤其是稀缺资源。例如,窗口句柄或数据库连接。 Finalize 专为非内存资源的非确定性处置而设计,通常在未调用 Dispose 时作为支持。

实现 Finalize 方法的一些准则:

仅对需要完成的对象实施 Finalize,因为 Finalize 方法会降低性能。 如果您需要 Finalize 方法,请考虑实现 IDisposable 以允许您的类型的用户避免调用 Finalize 方法的成本。 您的 Finalize 方法应该受到保护而不是公开。 您的 Finalize 方法应该释放该类型拥有的任何外部资源,但释放它拥有的那些。它不应引用任何其他资源。 CLR 不保证调用 Finalize 方法的顺序。正如 Daniel 在他的评论中指出的那样,这意味着 Finalize 方法不应该访问任何成员引用类型,因为它们可能拥有(或者有一天可能拥有)自己的终结器。 切勿直接对除类型的基类型以外的任何类型调用 Finalize 方法。 尽量避免在您的 Finalize 方法中出现任何未处理的异常,因为这将终止您的进程(在 2.0 或更高版本中)。 避免在您的 Finalizer 方法中执行任何长时间运行的任务,因为这会阻塞 Finalizer 线程并阻止执行其他 Finalizer 方法。

实现 Dispose 方法的一些准则:

在封装了明确需要释放的资源的类型上实施 dispose 设计模式。 在具有一个或多个保留资源的派生类型的基类型上实施 dispose 设计模式,即使基类型没有。 在实例上调用 Dispose 后,通过调用 GC.SuppressFinalize 方法阻止 Finalize 方法运行。此规则的唯一例外是必须在 Finalize 中完成 Dispose 未涵盖的工作的极少数情况。 不要假设 Dispose 会被调用。如果未调用 Dispose,则还应在 Finalize 方法中释放类型拥有的非托管资源。 当资源已被释放时,从该类型(Dispose 除外)的实例方法中引发 ObjectDisposedException。此规则不适用于 Dispose 方法,因为它应该可以多次调用而不会引发异常。 通过基本类型的层次结构传播对 Dispose 的调用。 Dispose 方法应释放此对象拥有的所有资源以及此对象拥有的任何对象。 您应该考虑在调用 Dispose 方法后不允许使用对象。重新创建一个已经被释放的对象是一个难以实现的模式。 允许多次调用 Dispose 方法而不引发异常。该方法在第一次调用后应该什么都不做。

【讨论】:

以上是关于既然 .NET 有一个垃圾收集器,为啥我们需要终结器/析构器/处置模式?的主要内容,如果未能解决你的问题,请参考以下文章

GC垃圾回收机制

GC垃圾回收机制

国外数据和垃圾收集

到底谁才是垃圾?

深入理解JVM——垃圾收集策略具体解释

为啥 PHP 的垃圾收集器会降低性能,没有它如何管理内存?