【问题标题】:How do I force release memory occupied by MemoryStream?如何强制释放 MemoryStream 占用的内存?
【发布时间】:2012-08-13 16:54:27
【问题描述】:

我有以下代码:

const int bufferSize = 1024 * 1024;
var buffer = new byte[bufferSize];
for (int i = 0; i < 10; i++)
{
    const int writesCount = 400;
    using (var stream = new MemoryStream(writesCount * bufferSize))
    {
        for (int j = 0; j < writesCount; j++)
        {
            stream.Write(buffer, 0, buffer.Length);
        }
        stream.Close();
    }
}

我在 32 位机器上运行。

第一次迭代完成得很好,然后在下一次迭代中我在news MemoryStream 的行上得到一个System.OutOfMemoryException 异常。

尽管using 声明,为什么没有回收之前的MemoryStream 内存?如何强制释放MemoryStream 使用的内存?

【问题讨论】:

  • 您是否尝试过强制垃圾收集器运行?
  • 顺便说一句,在写入MemoryStream 的实例之后,您将在不保存流内容的情况下将其释放到别处。如果写入后未使用流,为什么要写入流?
  • @Serge:这是一个说明问题的sn-p,这就是原因。
  • 其实你不必打电话给stream.Close()using 将接听电话stream.Dispose(),而后者又调用了Close()click here for further reading
  • 使用较小的缓冲区和/或重复使用它们。

标签: c# .net memory-management garbage-collection idisposable


【解决方案1】:

我认为问题不在于垃圾收集器没有做好它的工作。如果 GC 处于内存压力之下,它应该运行并回收您刚刚分配的 400 MB。

这更有可能归结为 GC 没有找到 连续的 400 MB 块。

相反,发生“内存不足”错误是因为进程无法 找到足够大的连续未使用页面部分 虚拟地址空间来做请求的映射。

您应该阅读 Eric Lippert 的博客文章 "Out Of Memory" Does Not Refer to Physical Memory

您最好同时进行以下操作

  1. 重用您分配的内存块(为什么要创建另一个大小完全相同的内存块)
  2. 分配小得多的块(小于 85KBs

在 Dotnet 4.5 之前,Dotnet 构建了两个堆,小对象堆 (SOH)大对象堆 (LOH)。请参阅 Brandon Bray 的Large Object Hearp Improvements in .NET 4.5。您的 MemoryStream 正在 LOH 中分配,并且在该过程期间未压缩(碎片整理),因此分配如此大量内存的多次调用更有可能引发 OutOfMemoryException

CLR 为分配管理两个不同的堆,即小对象 堆(SOH)和大对象堆(LOH)。任何更大的分配 大于或等于 85,000 字节在 LOH 上。复制大对象 有性能损失,因此 LOH 不像 SOH 那样被压缩。 另一个定义特征是 LOH 仅被收集 在第 2 代收集期间。这些都具有内置的 假设大对象分配不常见。

【讨论】:

  • +1 此外,该缓冲区将位于 LOH 中,从不进行碎片整理(与 SOH 相比)。
【解决方案2】:

看起来您的分配量超出了系统的处理能力。您的代码在我的机器上运行良好,但如果我这样更改:

const int bufferSize = 1024 * 1024 * 2;

我遇到和你一样的错误。

但是,如果我将目标处理器更改为 x64,那么代码就会运行,这似乎是合乎逻辑的,因为您可以处理更多内存。

本文详解:http://www.guylangston.net/blog/Article/MaxMemory 以及关于这个问题的一些信息:Maximum Memory a .NET process can allocate

【讨论】:

  • 对我来说似乎是根本问题
【解决方案3】:

首先,Dispose() 不保证会释放内存(它不会标记对象进行 GC 回收,在 MemoryStream 的情况下 - 它不会释放任何内容,因为 MemoryStream 没有非托管资源)。释放MemoryStream 使用的内存的唯一可靠方法是丢失对它的所有引用并等待垃圾收集发生(如果您有OutOfMemoryException - 垃圾收集器已经尝试但未能释放足够的内存)。此外,分配如此大的对象(任何 > 85000 字节)会产生一些后果 - 这些对象将进入大对象堆 (LOH),这可能会变得碎片化(并且无法压缩)。由于 .NET 对象必须占用连续的字节序列,这可能会导致您有足够的内存,但没有空间容纳大对象。在这种情况下,垃圾收集器无济于事。

这里的主要问题似乎是对 stream 对象的引用保留在堆栈上,从而阻止了 stream 对象的垃圾收集(即使强制垃圾收集也无济于事,因为 GC 认为该对象还活着,你可以检查这个创建一个WeakRefrence 给它)。重构此示例可以修复它:

    static void Main(string[] args)
    {
        const int bufferSize = 1024 * 1024 * 2;
        var buffer = new byte[bufferSize];
        for(int i = 0; i < 10; i++)
        {
            const int writesCount = 400;
            Write(buffer, writesCount, bufferSize);
        }
    }

    static void Write(byte[] buffer, int writesCount, int bufferSize)
    {
        using(var stream = new MemoryStream(writesCount * bufferSize))
        {
            for(int j = 0; j < writesCount; j++)
            {
                stream.Write(buffer, 0, buffer.Length);
            }
        }
    }

这是一个证明对象不能被垃圾回收的示例:

    static void Main(string[] args)
    {
        const int bufferSize = 1024 * 1024 * 2;
        var buffer = new byte[bufferSize];
        WeakReference wref = null;
        for(int i = 0; i < 10; i++)
        {
            if(wref != null)
            {
                // force garbage collection
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                // check if object is still alive
                Console.WriteLine(wref.IsAlive); // true
            }
            const int writesCount = 400;
            using(var stream = new MemoryStream(writesCount * bufferSize))
            {
                for(int j = 0; j < writesCount; j++)
                {
                    stream.Write(buffer, 0, buffer.Length);
                }
                // weak reference won't prevent garbage collection
                wref = new WeakReference(stream);
            }
        }
    }

【讨论】:

    【解决方案4】:

    当您确定有必要清理未引用的对象时,尝试强制进行垃圾回收。

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    

    另一种选择是将Stream 与外部存储一起使用:例如FileStream

    但是,在一般情况下,最好使用一个足够小的缓冲区(数组,分配一次)并将其用于读/写调用。避免在 .NET 中包含许多大型对象(请参阅 CLR Inside Out: Large Object Heap Uncovered)。

    更新

    假设writesCount是常量,那为什么不分配一个缓冲区并复用呢?

    const int bufferSize = 1024 * 1024;
    const int writesCount = 400;
    
    byte[] streamBuffer = new byte[writesCount * bufferSize];
    byte[] buffer = new byte[bufferSize];
    for (int i = 0; i < 10; i++)
    {
        using (var stream = new MemoryStream(streamBuffer))
        {
            for (int j = 0; j < writesCount; j++)
            {
                stream.Write(buffer, 0, buffer.Length);
            }
        }
    }
    

    【讨论】:

    • dispose 不应该立即释放内存吗?
    • Disposing 不会将内存标记为可被 GC 收集,请参阅stackoverflow.com/questions/11359131/…
    • 强制垃圾回收没有帮助。
    • @sharptooth,请看更新:代码只是一个建议。
    • 我最终将如何摆脱缓冲区?
    猜你喜欢
    • 2015-10-25
    • 2015-10-26
    • 2013-02-23
    • 1970-01-01
    • 2016-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-10
    相关资源
    最近更新 更多