6

我遇到了一个可终结对象的问题,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];
}

即使经过 10 次迭代,您也会发现内存消耗太高(从 700MB 到 1GB),并且随着更多迭代而变得更高。使用 WinDBG 附加到进程后,您会发现所有大对象都是无根的,但没有收集。

如果您SuppressFinalize()显式调用,情况会发生变化:即使在高压下,内存消耗也稳定在 300-400MB 左右,并且 WinDBG 显示没有无根对象,内存是空闲的。

所以问题是:它是框架中的错误吗?有什么合乎逻辑的解释吗?

更多细节:

每次迭代后,windbg 显示:

  • 终结队列为空
  • freachable 队列为空
  • 第 2 代包含来自先前迭代的对象(数百个)
  • 来自先前迭代的对象是无根的
4

2 回答 2

7

具有终结器的对象的行为方式与缺少终结器的对象不同。

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

第 1 代(和第 2 代)对象被认为是长期存在的,只有在第 1 代 GC 不足以释放足够的内存时才会考虑进行垃圾回收。我认为在你的测试过程中,Gen1 GC 总是足够的。

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

本质上,拥有一个终结器并且未能阻止 GC 调用它总是会将已经死亡的对象提升到长寿命堆,这不是一件好事。

因此,您应该正确处理您的 IDisposable 对象,并在不需要时避免使用终结器(如有必要,实施 IDisposable 并调用 GC.SuppressFinalize。)

编辑: 我没有很好地阅读代码示例:您的数据看起来像是要驻留在大对象堆 (LOH) 中,但实际上并非如此:您有很多包含在树的末尾小字节数组。

将短持续时间对象放在 LOH 中更糟糕,因为它们不会被压缩......因此,如果 CLR 无法找到足够长的空内存段以包含一大块数据。

于 2012-10-20T19:17:18.033 回答
1

我认为这背后的想法是,当您实现 IDisposable 时,这是因为您正在处理非托管资源,并且需要手动处置您的资源。

如果 GC 要调用 Dispose 或试图摆脱它,它也会刷新非托管的东西,这很可能在其他地方使用,GC 无法知道这一点。

如果 GC 要删除无根对象,您将失去对非托管资源的引用,这将导致内存泄漏。

所以......你被管理了,或者你没有。GC 根本没有好方法来处理未处理的 IDisposables。

于 2012-10-20T19:16:54.997 回答