垃圾收集器会打给IDisposable.Dispose给我吗?


134

.NET IDisposable模式 意味着,如果编写终结器并实现IDisposable,则终结器需要显式调用Dispose。这是合乎逻辑的,这是我在需要终结器的极少数情况下一直要做的事情。

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

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

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

是的,我意识到这听起来很愚蠢,所有逻辑都暗示这不会,但是我总是有两件事让我不确定。

  1. 几年前有人告诉我,实际上它会这样做,而且那个人在“知道自己的东西”方面有着非常可靠的记录。

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

尽管我已经阅读了很多有关此方面的内容,并且暗示了很多内容,但我始终无法找到该问题的明确答案“是”或“否”。

Answers:


121

.Net垃圾收集器在垃圾收集上调用对象的Object.Finalize方法。通过默认情况下这并没有什么,而且必须overidden如果您要释放更多的资源。

如果要释放资源(例如在“使用”或“最终尝试”块中),则不会自动调用Dispose,而必须显式调用Dispose。

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


35
实际上,我不相信GC如果没有被覆盖,它根本不会调用Object.Finalize。确定对象实际上没有终结器,并且终结被抑制-这使效率更高,因为该对象不需要位于终结/可到达队列中。
乔恩·斯基特

7
按照MSDN:msdn.microsoft.com/en-us/library/…您实际上无法“覆盖” C#中的Object.Finalize方法,编译器会生成错误:不要覆盖object.Finalize。而是提供一个析构函数。; 也就是说,您必须实现一个有效地充当终结器的析构函数。[此处仅出于完整性而添加,因为这是公认的答案,很可能会被阅读]
Sudhanshu Mishra 2014年

1
GC对不覆盖终结器的对象不执行任何操作。它不会放在Finalization队列中-不会调用Finalizer。
戴夫·布莱克

1
@dotnetguy-尽管原始的C#规范提到了“析构函数”,但实际上它被称为终结器-其机制与真正的“析构函数”对非托管语言的工作方式完全不同。
戴夫·布莱克

67

我想在他的评论中强调布莱恩的观点,因为它很重要。

终结器不是像C ++中那样的确定性析构函数。正如其他人所指出的那样,没有什么时候会被称为保证,而事实上,如果你有足够的内存,如果将永远被调用。

但是,关于终结器的坏处在于,正如Brian所说,它会使您的对象幸免于垃圾回收。这可能是不好的。为什么?

您可能知道也可能不知道,GC分为几代-第0代,第1代和第2代,以及大对象堆。拆分是一个宽松的术语-您可以获得一个内存块,但是有Gen 0对象开始和结束位置的指针。

思考过程是,您可能会使用许多寿命短的对象。因此,对于GC来说-Gen 0对象应该简单,快速。因此,当存在内存压力时,它要做的第一件事就是Gen 0集合。

现在,如果那不能解决足够的压力,则返回并进行Gen 1扫描(重做Gen 0),然后,如果仍然不够,则进行Gen 2扫描(重做Gen 1和Gen 0)。因此清理长寿命的对象可能要花一些时间,而且相当昂贵(因为在操作过程中可能会挂起线程)。

这意味着,如果您执行以下操作:

~MyClass() { }

无论如何,您的对象都将保留到第二代。这是因为GC无法在垃圾回收期间调用终结器。因此,必须要终结的对象被移到一个特殊的队列中,以由不同的线程(终结器线程-如果杀死它会导致各种不良情况发生)将其清除。这意味着您的对象会停留更长的时间,并可能导致更多的垃圾回收。

因此,所有这些只是为了传达您希望使用IDisposable尽可能清除资源的观点,并认真尝试寻找使用终结器的方法。这符合您应用程序的最大利益。


8
我同意您希望尽可能使用IDisposable,但您也应该有一个终结器,该终结器调用dispose方法。您可以在调用dispose方法之后在IDispose.Dispose中调用GC.SuppressFinalize(),以确保您的对象不会放入终结器队列中。
jColeson

2
世代编号为0-2,而不是1-3,但是您的帖子在其他方面还是不错的。不过,我要补充一点,您的对象所引用的任何对象,或者这些对象所引用的任何对象等等,也将受到保护,以防止在下一代发生垃圾回收(尽管不能最终确定)。因此,带有终结器的对象不应持有对终结器不需要的任何内容的引用。
超级猫


3
关于“您的对象,无论如何,都会活到第二代”。这是非常基础的信息!它节省了系统调试的大量时间,在该系统中,有很多短暂的Gen2对象“已准备好”进行终结处理,但由于大量的堆使用,因此从未终结过导致OutOfMemoryException。删除(甚至是空的)终结器,并将代码移至其他位置(来回移动),问题消失了,GC可以处理负载了。
卷笔刀

@CoryFoy“您的对象,无论如何,都将存活到第二代”是否有任何相关文档?
Ashish Negi

33

这里已经有很多很好的讨论了,我参加聚会有点晚了,但是我想自己补充一点。

  • 垃圾收集器将永远不会直接为您执行Dispose方法。
  • GC 在需要时执行终结器。
  • 用于具有终结器的对象的一种常见模式是让其调用一种方法,该方法按照惯例定义为Dispose(bool dispose)传递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不应执行任何操作)
  • 正确管理一次性对象的继承层次结构可能会变得非常复杂,尤其是如果不同的层引入了新的Disposable和不受管理的资源时。在上面的模式中,Dispose(bool)是虚拟的,允许对其进行重写以便可以对其进行管理,但是我发现它容易出错。

我认为,最好完全避免使用直接包含一次性引用和可能需要最终确定的本机资源的任何类型。SafeHandles通过将本机资源封装到内部提供自己的终结处理的一次性资源中,提供了一种非常干净的方式(以及许多其他好处,例如在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表示///在我看来,最好完全避免使用直接包含可处理引用和本机资源的任何类型,这些类型可能都需要终结器。定案。
超级猫


1
关于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

3

并非您所描述的那样,但是GC将调用终结器如果您有,器。

然而。下一个垃圾回收而不是被回收,对象将进入终结队列,所有内容都被收集,然后调用终结器。之后的下一个集合将被释放。

根据您应用程序的内存压力,您可能会暂时没有gc用于生成该对象。因此,就文件流或db连接而言,您可能需要等待一段时间才能在终结器调用中释放非托管资源一段时间,从而导致一些问题。


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

1

GC将不会调用处置。它可能会调用您的终结器,但即使在所有情况下也无法保证。

有关解决此问题的最佳方法,请参见本文


0

关于IDisposable的文档对行为以及示例代码进行了非常清晰,详细的说明。GC不会Dispose()在接口上调用该方法,但会为您的对象调用终结器。


0

如果您有一个实现IDispose的对象,则IDisposable模式主要由开发人员创建,如果开发人员应该实现 using在对象的上下文周围关键字,直接调用Dispose方法。

模式的故障保护是实现终结器,调用Dispose()方法。如果不这样做,则可能会造成一些内存泄漏,即:如果创建一些COM包装程序,并且从不调用System.Runtime.Interop.Marshall.ReleaseComObject(comObject)(将其放置在Dispose方法中)。

除了跟踪包含终结器的对象并由GC将它们存储在Finalizer表中,并在GC进行一些清理启发式调用时调用它们之外,clr中没有什么魔术可以自动调用Dispose方法。

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.