我注意到我的应用程序内存耗尽的速度超出应有的速度。它创建了许多每个几兆字节的字节数组。但是,当我查看vmmap的内存使用情况时,似乎.NET为每个缓冲区分配的内容远远超过了需要。确切地说,当分配9兆字节的缓冲区时,.NET会创建一个16兆字节的堆。剩余的7兆字节不能用于创建另一个9兆字节的缓冲区,因此.NET会创建另外的16兆字节。所以每个9MB缓冲区浪费了7MB的地址空间!
这是一个在32位.NET 4中分配106个缓冲区后抛出OutOfMemoryException的示例程序:
using System.Collections.Generic;
namespace CSharpMemoryAllocationTest
{
class Program
{
static void Main(string[] args)
{
var buffers = new List<byte[]>();
for (int i = 0; i < 130; ++i)
{
buffers.Add(new byte[9 * 1000 * 1024]);
}
}
}
}
请注意,您可以将阵列的大小增加到16 * 1000 * 1024,并在内存不足之前仍然分配相同数量的缓冲区。
VMMap显示:
另请注意,托管堆的总大小与总通信大小之间几乎有100%的差异。 (1737MB vs 946MB)。
在.NET上是否存在可靠的解决此问题的方法,即我是否可以强制运行时分配不超过我实际需要的内容,或者可以用于几个连续缓冲区的更大的托管堆?
答案 0 :(得分:6)
CLR在内部分段内存。从您的描述中可以看出,16 MB分配是段,您的阵列是在这些段中分配的。剩下的空间是保留的,在正常情况下不会真正浪费,因为它将用于其他分配。如果您没有适合剩余块的任何分配,则这些分配基本上是开销。
由于您的数组是使用连续内存分配的,因此您只能使用一个段中的一个,因此在这种情况下会产生开销。
默认段大小为16 MB,但如果分配大于该值,则CLR将分配更大的段。我不知道细节,但是如果我分配20 MB Wmmap显示我24 MB的段。
减少开销的一种方法是在可能的情况下进行符合段大小的分配。但请记住,这些是实施细节,并且可能随着CLR的任何更新而改变。
答案 1 :(得分:3)
CLR一次性从操作系统中保留了16MB的块,但只占用了9MB。
我相信你期待9MB和9MB在一堆。困难在于变量现在被分成2堆。
Heap 1 = 9MB + 7MB
Heap 2 = 2MB
我们现在遇到的问题是,如果原始的9MB
被删除,我们现在有2个堆我们无法整理,因为内容是在堆上共享的。
为了提高性能,方法是将它们放在单个堆中。
如果您担心内存使用情况,请不要这样做。内存使用对.NET来说并不坏,因为如果没有人使用它,那么问题是什么? GC会在某个时刻启动,内存将被整理。 GC只会启动
但是内存使用情况,特别是在这个例子中不应该是一个问题。内存使用会停止CPU周期的发生。否则,如果它不断整理内存,你的CPU会很高,而你的进程(以及你机器上的所有其他进程)运行速度会慢得多。
答案 2 :(得分:3)
伙伴系统堆管理算法的老化症状,其中2的幂用于递归地在二叉树中拆分每个块,因此对于9M,下一个大小是16M。如果将阵列大小降低到8mb,您将看到使用量减少一半。这不是一个新问题,本机程序员也会处理它。
小对象池(小于85,000字节)的管理方式不同,但是在9MB时,您的阵列位于大对象池中。从.NET 4.5开始,大对象堆不参与压缩,大对象立即被提升为第2代。
您不能强制使用该算法,但您可以通过确定使用哪种大小来最有效地填充二进制段来强制您的用户代码。
如果需要使用9 MB数组填充进程空间,请执行以下操作:
回收伙伴系统堆的碎片部分是一个具有对数返回的优化(即,你几乎耗尽了内存)。除非您的数据大小是固定的,否则在某些时候您将不得不移动到64位,除非您的数据大小是固定的。