垃圾回收器会为我调用IDisposable.Dispose吗?

151

.NET的IDisposable模式暗示,如果你编写了一个终结器,并实现了IDisposable接口,那么你的终结器需要显式地调用Dispose方法。这是合乎逻辑的,在极少数情况下需要使用终结器时我一直都是这样做的。

但是,如果我只是这样做会发生什么:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

不要实现终结器或其他任何东西。那么框架会为我调用Dispose方法吗?

是的,我知道这听起来很蠢,所有的逻辑都表明它不会,但我一直有两个事情让我不确定。

  1. 几年前有人告诉我它确实会这样做,而且那个人有非常扎实的"知识储备"。

  2. 编译器/框架根据您实现的接口执行其他'魔法'操作(例如: foreach、基于属性的扩展方法、序列化等),因此这也可能是'魔法'。

虽然我读了很多关于它的东西,并且有很多暗示,但我从来没有找到一个 明确的 是或否 的答案。

9个回答

137

.Net垃圾回收机制会在进行垃圾回收时调用对象的Object.Finalize方法。默认情况下,此方法不执行任何操作,如果想要释放其他资源,必须对其进行重写。

Dispose不会自动调用,如果要释放资源,必须显式地调用它,例如在'using'或'try finally'代码块中。

有关更多信息,请参见http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx


40
实际上,如果没有被重写,我不相信GC会调用Object.Finalize。该对象被确定为实际上没有终结器,并且终结被抑制,这使得它更加高效,因为对象不需要在终结/可达队列中。 - Jon Skeet
9
根据MSDN的说法:http://msdn.microsoft.com/en-us/library/system.object.finalize%28v=vs.110%29.aspx,在C#中,你实际上不能“覆盖”Object.Finalize方法,编译器会生成错误:不要重写object.Finalize。相反,提供一个析构函数; 即,你必须实现一个有效地充当Finalizer的析构函数。[这里只是为了完整性而添加,因为这是公认的答案,最可能被阅读] - Sudhanshu Mishra
2
垃圾回收器对于没有覆盖终结器的对象不会执行任何操作。它不会被放置在终结队列中,也不会调用任何终结器。 - Dave Black
1
尽管原始的C#规范提到了“析构函数”,但它实际上被称为终结器(Finalizer)- 它的机制与非托管语言中真正的“析构函数”完全不同。 - Dave Black

75

我想要强调Brian在评论中的观点,因为它很重要。

终结器不像C++中的确定性析构函数。正如其他人指出的那样,不能保证它何时会被调用,事实上,如果你有足够的内存,就可能永远不会被调用。

但终结器的问题在于,正如Brian所说的,它会导致对象在垃圾回收时生存下来,这可能是不好的。为什么?

您可能知道或者不知道,GC被分成几代——Gen 0,1和2,以及大对象堆。"分割"是一个宽泛的术语——你得到一块内存,但有指针指向Gen 0对象的起始和结束位置。

思路是你可能会使用许多短命的对象。因此,这些应该是GC轻松快速处理的-Gen 0对象。因此,当存在内存压力时,第一件事就是进行Gen 0收集。

现在,如果这不能解决足够的压力,那么它会返回并执行Gen 1扫描(重新执行Gen 0),然后如果仍然不够,它会执行Gen 2扫描(重新执行Gen 1和Gen 0)。因此,清理长寿命对象可能需要一段时间,并且可能相当昂贵(因为在操作期间您的线程可能会被暂停)。

这意味着,如果您像这样做:

~MyClass() { }

无论如何,您的对象都将存在于第二代。这是因为垃圾回收无法在垃圾回收期间调用终结器。因此,必须终止的对象被移动到一个特殊的队列中,由另一个线程(终结器线程 - 如果您杀死它会导致各种糟糕的事情发生)进行清除处理。这意味着您的对象会存在更长时间,并且可能会强制执行更多的垃圾回收。

因此,所有这些只是为了强调您应该尽可能使用IDisposable来清理资源,并且严肃地尝试找到绕过使用终结器的方法。这符合您的应用程序最佳利益。


11
我同意你希望尽可能使用IDisposable,但你也应该有一个终结器来调用一个dispose方法。在调用你的dispose方法后,你可以在IDisposable.Dispose中调用GC.SuppressFinalize()来确保你的对象不会进入终结器队列。 - jColeson
2
代际的编号为0-2,而不是1-3,但您的帖子在其他方面很好。我想补充一下,您的对象引用的任何对象,或者那些引用的任何对象等等,也将受到垃圾回收的保护(尽管不受终结的保护),直到另一个代际。因此,具有终结器的对象不应持有对任何不需要进行终结的内容的引用。 - supercat
[一种]用于生成数字的参考(类型)[的]引用。 (https://dev59.com/jnE95IYBdhLWcg3wkeof) - Lightness Races in Orbit
4
关于“无论如何,您的对象都将存活到第二代。” 这是非常基本的信息!它节省了大量调试系统的时间,在该系统中有许多短寿命的Gen2对象,“准备”进行终结,但由于堆使用过重而从未终结,导致OutOfMemoryException。去掉(甚至是空的)终结器并将代码移动(解决问题),问题就消失了,垃圾回收器能够处理负载。 - sharpener
@CoryFoy "无论如何,您的对象都将存活到第二代" 这方面有文档吗? - Ashish Negi

35

这里已经有很多好的讨论了,我有点晚来参加,但我想自己添加一些观点。

  • 垃圾回收器永远不会直接执行Dispose方法。
  • GC会在感觉需要时执行终结器。
  • 一种常见的模式是,对于具有终结器的对象,将其调用一个按照约定定义为Dispose(bool disposing)的方法,并传递false以指示该调用是由于终结而不是显式Dispose调用。
  • 这是因为在终止对象时不能对其他托管对象进行任何假设(它们可能已经被终止)。

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

这是简单的版本,但是这种模式有很多微妙之处可能会让你陷入困境。
IDisposable.Dispose的合同表明,可以安全地多次调用它(在已经处理过的对象上调用Dispose应该什么都不做)。
如果不同层引入新的可处理和未管理的资源,正确地管理可处理对象的继承层次结构会变得非常复杂。在上面的模式中,Dispose(bool)是虚拟的,以允许覆盖它,以便可以进行管理,但我发现这很容易出错。
在我看来,最好完全避免直接包含既包含一次性引用又包含可能需要最终处理的本机资源的任何类型。 SafeHandle通过将本地资源封装到可处理资源中,并在内部提供自己的终结(以及许多其他好处,例如在P / Invoke期间删除窗口,其中由于异步异常可能会丢失本机句柄),提供了非常清晰的方法。
简单地定义一个SafeHandle即可实现此目的。

private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

允许您简化包含类型为:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

1
SafeHandleZeroOrMinusOneIsInvalid类来自哪里?它是内置的.net类型吗? - Orion Edwards
+1 for //在我看来,最好完全避免直接包含既包含可处理的引用又包含可能需要终结的本机资源的任何类型。//唯一应该具有终结器的未密封类是其目的集中在终结上的类。 - supercat
1
@OrionEdwards 是的,请参见http://msdn.microsoft.com/en-us/library/microsoft.win32.safehandles.safehandlezeroorminusoneisinvalid.aspx - Martin Capodici
2
关于此示例中调用 GC.SuppressFinalize 的问题。在这种情况下,只有当 Dispose(true) 成功执行时才应该调用 SuppressFinalize。如果在抑制终结但未清理所有资源(尤其是非托管资源)之前,Dispose(true) 在某个时刻失败,则仍然希望发生终结以尽可能多地清理资源。最好将 GC.SuppressFinalize 的调用移到 Dispose() 方法中,在调用 Dispose(true) 之后。请参阅《框架设计准则》和此文章 - BitMask777

6

我不这么认为。您可以控制何时调用Dispose,这意味着您可以理论上编写处理代码并对其他对象的存在做出假设。您无法控制何时调用终结器,因此让终结器自动代表您调用Dispose可能会有问题。


编辑:我离开并进行了测试,只是为了确保:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

在处理过程中对可用对象进行假设可能是危险和棘手的,特别是在终结期间。 - Scott Dorman

6

在您所描述的情况下不会发生这种情况,但是如果您有一个 Finalizer,垃圾回收器将为您调用它。

然而。下一次进行垃圾回收时,对象不会被收集,而是进入终结队列,所有内容都被收集后,才会调用它的终结器。之后的垃圾回收会释放它。

根据您的应用程序内存压力的情况,可能会有一段时间没有针对该对象代的垃圾回收。因此,在文件流或数据库连接等情况下,您可能需要等待一段时间,直到未受管资源在终结器调用中被释放,从而导致一些问题。


2

垃圾回收机制不会调用dispose方法。它可能会调用您的finalizer,但在某些情况下甚至无法保证这一点。

请参阅此文章,了解处理此问题的最佳方法。


1

不,它没有被调用。

但是这使得不容易忘记处理你的对象。只需使用using 关键字即可。

我为此进行了以下测试:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

1
这是一个例子,如果你不使用<code>using</code>关键字,它就不会被调用...而这段代码已经有9年了,生日快乐! - penyaskito

0

IDisposable 的文档提供了非常清晰和详细的解释,以及示例代码。垃圾回收器(GC)不会调用接口上的 Dispose() 方法,但它会调用您对象的终结器。


0

IDisposable模式主要是为了让开发人员调用,如果您有一个实现IDispose的对象,开发人员应该在对象的上下文中实现using关键字或直接调用Dispose方法。

该模式的故障安全机制是实现终结器来调用Dispose()方法。如果您不这样做,可能会创建一些内存泄漏,例如:如果您创建了一些COM包装器并从未调用System.Runtime.InteropServices.Marshal.ReleaseComObject(comObject)(这将放置在Dispose方法中)。

CLR中没有自动调用Dispose方法的魔法,除了跟踪包含终结器的对象并将它们存储在GC的终结器表中,并在GC触发某些清理启发式时调用它们。


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