【问题标题】:Dispose and Finalizer not called, OutOfMemoryException occurs未调用 Dispose 和 Finalizer,发生 OutOfMemoryException
【发布时间】:2014-05-15 12:10:38
【问题描述】:

我正在尝试编写一个类来包装使用Marshal.AllocHGlobal 分配的缓冲区。我实现了IDisposable 接口,并添加了一个终结器,当我不再需要它时(当对象超出范围时)应该释放内存。

当我测试类时,GC 不会调用我的类的终结器或 Dispose 方法,即使它们超出了范围。结果,我得到了OutOfMemoryException

GC为什么不调用finalizer,为什么内存没有被释放?

这是一个说明问题的简短示例。在示例中,没有任何内容写入控制台(Unhandled Exception: OutOfMemoryException. 除外)

class Buffer : IDisposable
{
    public IntPtr buf { get; set; }

    public Buffer()
    {
        buf = Marshal.AllocHGlobal(4 * 1024 * 1024);
    }

    ~Buffer()
    {
        Console.WriteLine("Finalizer called");
        Dispose(false);
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose called");
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    internal virtual void Dispose(bool disposing)
    {
        if (buf != IntPtr.Zero)
        {
            Console.WriteLine("Releasing memory");
            Marshal.FreeHGlobal(buf);
            buf = IntPtr.Zero;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        while(true)
        {
            Buffer b = new Buffer();
            Thread.Sleep(20);
        }
    }
}

编辑:这是我的测试程序崩溃时的 .NET 性能计数器:

【问题讨论】:

  • 您分配内存的速度超过了垃圾收集器通过调用范围外对象的终结器来回收它的速度。
  • 内存不足异常是物理内存不足吗?
  • @Matt 不,我有 8 GB 的 RAM,测试程序最大使用 1.8 GB(当它崩溃时)。即使它使用 1.8GB,我仍然有大约 4.3GB 的可用物理内存。
  • @martin_costello GC 甚至没有尝试回收任何东西。当它注意到可用内存变低时,它不应该启动吗?
  • .NET CLR 2 和 .NET CLR 4 具有不同的垃圾收集器。你用的是哪个版本?

标签: c# memory garbage-collection


【解决方案1】:

您需要告诉垃圾收集器,您的具有单个 IntPtr 字段的非常小的托管对象在非托管内存方面具有很高的成本。目前,垃圾收集器完全没有意识到每个小型托管对象都使用大量非托管内存并且没有理由执行任何收集。

分配非托管内存时可以使用GC.AddMemoryPressure,释放非托管内存时可以使用GC.RemoveMemoryPressure

【讨论】:

  • 这行得通。感谢您的回答和解释。
【解决方案2】:

当下列条件之一为真时发生垃圾收集:

  1. 系统物理内存不足。
  2. 托管堆上分配的对象使用的内存 超过可接受的阈值。这个阈值是连续的 随着流程的运行进行调整。
  3. GC.Collect 方法被调用。在几乎所有情况下,您都不 必须调用这个方法,因为垃圾收集器运行 不断地。此方法主要用于特殊情况 和测试。

而且垃圾收集器只在托管堆上跟踪内存,所以对于这个程序,唯一可以触发 GC 的条件是第一个。

我编译的程序,如果目标CPU是x86,当进程的私有字节达到2G左右时会出现内存溢出异常。当我运行程序时,我注意到私有字节增加很快,但工作集增加非常缓慢,而且系统物理内存使用量增加非常缓慢。

作为私有字节和工作集,post 解释说: 私有字节是指进程可执行文件要求的内存量 - 不一定是它实际使用的内存量。它们是“私有的”,因为它们(通常)排除了内存映射文件(即共享 DLL)。但是 - 这里有一个问题 - 它们不一定排除这些文件分配的内存。无法判断私有字节的变化是由于可执行文件本身还是由于链接库。私有字节也不仅仅是物理内存;它们可以被分页到磁盘或备用页面列表中(即不再使用,但也没有分页)。

工作集是指进程使用的总物理内存 (RAM)。但是,与私有字节不同,这还包括内存映射文件和各种其他资源,因此它的测量精度甚至不如私有字节。这与任务管理器的“内存使用情况”中报告的值相同,并且近年来一直是无数混乱的根源。工作集中的内存是“物理的”,因为它可以在没有页面错误的情况下被寻址;但是,备用页面列表仍然物理上位于内存中,但未在工作集中报告,这就是为什么您在最小化应用程序时可能会看到“Mem Usage”突然下降的原因。

Marshal.AllocHGlobal 只是增加了私有字节,但工作集仍然很小,也不会触发 GC。

请参考:Fundamentals of Garbage Collection

【讨论】:

    【解决方案3】:

    IDisposable 是声明性的,dispose 方法仅在垃圾收集实际发生时调用。

    Yoy 可以强制进行垃圾收集,为此你需要调用

    GC.收集

    http://msdn.microsoft.com/en-us/library/xe0c2357(v=vs.110).aspx

    我还建议使用性能计数器来查看应用内存消耗情况,并查看是否已调用 GC。看这里怎么做http://msdn.microsoft.com/en-us/library/x2tyfybc(v=vs.110).aspx

    【讨论】:

    • 如果我在循环内调用GC.Collect(),它确实有效,但问题是为什么 GC 在看到我(几乎)内存不足时不会自动收集?
    • 您需要使用性能计数器来查看是否确实调用了 GC,也许这不是因为您仍然对您的对象持有敬意,例如在某种 IOC 容器中。
    • 我已经编辑了我的问题,以提供我的应用的性能计数器的详细信息。正如您从我发布的代码中看到的那样,我没有对我创建的缓冲区进行其他引用。缓冲区被创建然后被遗忘,而不是存储在任何地方。
    猜你喜欢
    • 2017-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-08
    • 1970-01-01
    • 2013-04-01
    • 1970-01-01
    • 2017-03-17
    相关资源
    最近更新 更多