.NET垃圾收集器之谜
在我的工作中,我们遇到了OutOfMemoryExceptions的问题。 我写了一段简单的代码来模仿一些行为,我最终得到了以下谜团。 看看这个简单的代码,当内存耗尽时会爆炸。
class Program { private static void Main() { List list = new List(200000); int iter = 0; try { for (;;iter++) { list.Add(new byte[10000]); } } catch (OutOfMemoryException) { Console.WriteLine("Iterations: " + iter); } } }
在我的机器上它结束了
Iterations: 148008
然后我在每千次迭代后添加了一个GC.Collect
调用循环:
//... for (;;iter++) { list.Add(new byte[10000]); if (iter % 1000 == 0) GC.Collect(); } //...
并且惊喜:
Iterations: 172048
当我在每10次迭代后调用GC.Collect
,我甚至得到了193716次循环。 有两件奇怪的事情:
-
如何手动调用
GC.Collect
产生如此严重的影响(分配多达30%)? -
当没有“丢失”参考时(我甚至预设了List的容量),GC可以收集什么呢?
垃圾收集过程的一部分是压缩阶段。 在此阶段,分配的内存块被移动以减少分类。 分配内存时,并不总是在最后一块分配的内存中断后立即分配。 因此,您可以进一步挤压,因为垃圾收集器通过更好地利用可用空间来腾出更多空间。
我正在尝试运行一些测试,但我的机器无法处理它们。 尝试一下,它会告诉GC
将内存中的对象固定下来,这样它们就不会被移动
byte[] b = new byte[10000]; GCHandle.Alloc(b, GCHandleType.Pinned); list.Add(b);
至于你的评论,当GC
移动时,它不会擦掉任何东西,它只是更好地利用所有内存空间。 让我们尝试并简化这一点。 当你第一次分配你的字节数组时,让我们说它从点0到10000插入内存。下次你分配字节数组时,不保证从10001开始,它可能从10500开始。所以现在您有499个未使用的字节,并且您的应用程序将不会使用它。 因此,当GC
压缩时,它会将10500arrays移动到10001,以便能够使用额外的499字节。 再次,这是简化的方式。
根据您使用的CLR,可能涉及一些大对象堆问题。
看一下这篇文章,它解释了大块分配的问题(并且具有200000个项目的列表肯定是一个大块,另一个可能是也可能不是,当一些arrays达到8k时似乎被放入LOH, 85k后的其他人)。
http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/
CLR偶尔会在LOH上放置数组。 如果您通过WinDbg查看内存转储,您将看到有少于85,000字节的数组。 它是无证件的行为 – 但这只是它的工作方式。
您正在获取OutOfMemoryErrors,因为您正在分割LOH堆并且LOH堆从未被压缩。
关于你的问题:
2)当没有“丢失”参考时(我甚至预设了List的容量),GC可以收集什么?
您传递的new byte[10000]
会覆盖引用以添加到列表中。 编译局部变量并将其分配给new byte[10000]
。 对于循环中的每次迭代,您都会创建一个具有预定义大小10000的新byte [],并将其分配给局部变量。 该变量的任何先前值都将被覆盖,并且该下一次GC运行以生成该变量时,该内存符合收集条件(在这种情况下,可能是LOH)。
我在.NET中有类似的问题,我的byte []有随机大小。
我试过两种方法:
-
编写自己的堆管理器(使用一个大缓冲区分配内存并调整指针)
-
使用内存映射文件(在我看来更好的解决方案)
如果可能的话,你可以尝试.NET 4.5 http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements -用于客户端和服务器,apps.aspx