关于.net:垃圾收集器会致电IDisposable。为我处理?

关于.net:垃圾收集器会致电IDisposable。为我处理?

Will the Garbage Collector call IDisposable.Dispose for me?

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

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

1
2
3
4
class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

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

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

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

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

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


    .Net垃圾收集器在垃圾收集上调用对象的Object.Finalize方法。默认情况下,此操作不执行任何操作,如果要释放其他资源,则必须将其忽略。

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

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


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

    终结器不是像C ++中那样的确定性析构函数。正如其他人指出的那样,不能保证何时调用它,实际上不能保证是否有足够的内存可以调用。

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

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

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

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

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

    1
    ~MyClass() { }

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

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


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

    • 垃圾收集器将永远不会直接为您执行Dispose方法。
    • GC会在需要时执行终结器。
    • 用于具有终结器的对象的一种常见模式是让其调用一种方法,该方法按照惯例定义为Dispose(bool dispose),传递false来表示该调用是由于终结器而不是显式的Dispose调用。
    • 这是因为在完成对象时对其他托管对象进行任何假设都是不安全的(它们可能已经完成)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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就可以做到这一点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private class SomeSafeHandle
     : SafeHandleZeroOrMinusOneIsInvalid {
     public SomeSafeHandle()
      : base(true)
      { }

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

    允许您将包含类型简化为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class SomeObject : IDisposable {
     SomeSafeHandle _SomeSafeHandle;
     FileStream _SomeFileStream;
     // Something useful here
     public virtual void Dispose() {
      _SomeSafeHandle.Dispose();
      _SomeFileStream.Dispose();
     }
    }

    我不这么认为。您可以控制何时调用Dispose,这意味着从理论上讲您可以编写对(例如)其他对象的存在进行假设的处理代码。您无法控制何时调用终结器,因此让终结器代表您自动调用Dispose会很困难。

    编辑:我去测试,只是为了确保:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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");
        }
    }

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

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

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


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

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


    不,它没有被调用。

    但这很容易使您不要忘记布置对象。只需使用using关键字。

    为此,我做了以下测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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!");
        }

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

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

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


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


    推荐阅读