即使没有根据,为什么不使用终结器收集对象?

6op*_*puc 6 .net c# garbage-collection

我遇到了一个问题,即最终化的对象GC如果Dispose()未被显式调用则不会被收集.我知道Dispose()如果一个对象实现IDisposable,我应该显式调用,但我一直认为依赖框架是安全的,当一个对象变得未被引用时,它可以被收集.

但在使用windbg/sos/sosex进行一些实验后,我发现如果没有为可终结对象调用GC.SuppressFinalize(),它就不会被收集,即使它没有被引导.因此,如果您广泛使用可终结对象(DbConnection,FileStream等)并且未明确处理它们,则可能会遇到过高的内存消耗甚至OutOfMemoryException.

这是一个示例应用程序:

public class MemoryTest
{
    private HundredMegabyte hundred;

    public void Run()
    {
        Console.WriteLine("ready to attach");
        for (var i = 0; i < 100; i++)
        {
            Console.WriteLine("iteration #{0}", i + 1);
            hundred = new HundredMegabyte();
            Console.WriteLine("{0} object was initialized", hundred);
            Console.ReadKey();
            //hundred.Dispose();
            hundred = null;
        }
    }

    static void Main()
    {
        var test = new MemoryTest();
        test.Run();
    }
}

public class HundredMegabyte : IDisposable
{
    private readonly Megabyte[] megabytes = new Megabyte[100];

    public HundredMegabyte()
    {
        for (var i = 0; i < megabytes.Length; i++)
        {
            megabytes[i] = new Megabyte();
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~HundredMegabyte()
    {
        Dispose(false);
    }

    private void Dispose(bool disposing)
    {
    }

    public override string ToString()
    {
        return String.Format("{0}MB", megabytes.Length);
    }
}

public class Megabyte
{
    private readonly Kilobyte[] kilobytes = new Kilobyte[1024];

    public Megabyte()
    {
        for (var i = 0; i < kilobytes.Length; i++)
        {
            kilobytes[i] = new Kilobyte();
        }
    }
}

public class Kilobyte
{
    private byte[] bytes = new byte[1024];
}
Run Code Online (Sandbox Code Playgroud)

即使经过10次迭代,您仍然可以发现内存消耗过高(从700MB到1GB),并且随着迭代次数的增加会更高.使用WinDBG附加到进程后,您可以发现所有大对象都是无根,但未收集.

如果你SuppressFinalize()明确调用情况会改变:即使在高压下,内存消耗也会稳定在300-400MB左右,而WinDBG显示没有无根对象,内存是免费的.

所以问题是:它是框架中的错误吗?有合理的解释吗?

更多细节:

每次迭代后,windbg显示:

  • 终结队列为空
  • 可释放的队列是空的
  • 第2代包含先前迭代中的对象(Hundred)
  • 以前迭代中的对象是无根的

Eil*_*aee 7

具有终结器的对象的行为与缺少对象的对象的行为不同.

当GC发生并且尚未调用SuppressFinalize时,GC将无法收集实例,因为它必须执行Finalizer.因此,执行终结器,并且对象实例被提升为第1代(在第一个GC中幸存的对象),即使它已经没有任何生命引用.

第1代(和Gen2)对象被认为是长期存在的,只有在Gen1 GC不足以释放足够的内存时才会考虑进行垃圾收集.我认为在测试期间,Gen1 GC总是足够的.

这种行为对GC性能有影响,因为它否定了通过几个générations(你在gen1中有短持续时间对象)带来的优化.

从本质上讲,拥有一个Finalizer并且无法阻止GC调用它将始终将已经死亡的对象提升为长寿堆,这不是一件好事.

因此,您应该正确处理您的IDisposable对象,如果没有必要,请避免使用终结器(如果需要,可以实现IDisposable并调用GC.SuppressFinalize.)

编辑: 我没有充分阅读代码示例:您的数据看起来像是存在于大对象堆(LOH)中,但事实上并非如此:您有很多小的引用数组包含在树的末尾是小字节数组.

将短期对象放入LOH更糟糕,因为它们不会被压缩...因此,如果CLR无法找到足够长的空内存段,则可以使用大量可用内存运行OutOfMemory一大块数据.

  • 如果它们都处于空闲状态,那么您对系统没有压力,因此不需要GC.GC对压力做出反应(内存段填满,来自操作系统的低内存通知等),并在这些时间收集.它不会决定你的进程已经安静了一段时间,所以GC应该运行.Gen 2产品价格昂贵,因此GC会在必要时避免使用它们.这就是为什么你的对象是无根和无法收集的原因. (2认同)