.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次循环。 有两件奇怪的事情:

  1. 如何手动调用GC.Collect产生如此严重的影响(分配多达30%)?

  2. 当没有“丢失”参考时(我甚至预设了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