如何强制释放MemoryStream所占用的内存?

17

我有以下代码:

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位机器上运行。第一次迭代成功完成,但在下一次迭代中,在 new MemoryStream 的行上会出现 System.OutOfMemoryException 异常。

为什么之前的MemoryStream尽管使用了using语句,但内存没有被回收?如何强制释放MemoryStream占用的内存?


顺便提一下,在向“MemoryStream”的实例写入后,您正在处置它,而没有将流的内容保存在其他地方。如果写入后未使用该流,为什么要写入该流? - Sergey Vyacheslavovich Brunov
3
@Serge: 这只是一个片段,用来说明问题。 - sharptooth
1
实际上,您不必调用 stream.Close()using 语句将调用 stream.Dispose(),而后者已经调用了 Close()点击此处以获取更多信息 - Manuzor
1
使用较小的缓冲区和/或重复使用它们。 - CodesInChaos
@CodesInChaos,我完全同意你的观点!一种缓冲区可以重复使用。另一个选择是使用外部(例如文件)存储大块数据。 - Sergey Vyacheslavovich Brunov
显示剩余6条评论
4个回答

17

我认为问题不在于垃圾收集器没有完成其工作。如果GC面临内存压力,它应该运行并回收你刚刚分配的400 MB。

更有可能是因为GC找不到一个{连续的400 MB}块。

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

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

你最好做到以下两点

  1. 重复使用你已经分配的内存块(为什么要创建一个完全相同大小的内存块)
  2. 分配较小的块(小于85KBs

在Dotnet 4.5之前,Dotnet构建了两个堆:Small Object Heap (SOH)Large Object Heap (LOH)。请参阅Brandon Bray的Large Object Hearp Improvements in .NET 4.5。您的MemoryStream是在LOH中分配的,并且在整个过程中不会被压缩(碎片整理),这使得多次调用以分配这么大量的内存更容易抛出OutOfMemoryException

CLR管理两种不同的分配堆,即小对象堆(SOH)和大对象堆(LOH)。任何大于或等于85,000字节的分配都会进入LOH。复制大对象会带来性能损失,因此LOH不像SOH那样被压缩。另一个定义特征是,只有在第二代收集期间才会对LOH进行收集。总体而言,这些都具有内置的假设,即大对象分配不频繁。


1
此外,该缓冲区将位于 LOH 中,该区域永远不会被碎片化(与 SOH 相反)。 - oleksii

1

看起来你分配的资源超出了系统的承受范围。你的代码在我的电脑上运行良好,但如果我像这样更改它:

const int bufferSize = 1024 * 1024 * 2;

我遇到了和你一样的错误。

但是如果我将目标处理器更改为x64,那么代码就可以运行了,这似乎很合理,因为你可以访问更多的内存。

关于这篇文章的详细解释:http://www.guylangston.net/blog/Article/MaxMemory 以及有关此问题的一些信息:最大内存.NET进程可以分配


对我来说似乎是潜在的问题。 - Liam

1

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

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难道不应该立即释放内存吗? - Filip
1
释放资源并不会标记内存为可由GC收集,请参见https://dev59.com/32XWa4cB1Zd3GeqPQcxh。 - Maarten
强制垃圾回收并不能帮助。 - sharptooth
@sharptooth,请注意更新:代码只是一个建议。 - Sergey Vyacheslavovich Brunov
最后我该如何清除缓冲区? - sharptooth
@sharptooth,字节数组将被垃圾回收。你可以尝试在for循环之后强制进行垃圾回收。我提供的代码分配了一个大缓冲区;旧代码在每次循环体执行时都会分配相同的大缓冲区。你试过这段代码吗? - Sergey Vyacheslavovich Brunov

1
首先,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);
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接