代码之家  ›  专栏  ›  技术社区  ›  Elephantik

.NET垃圾收集器之谜

  •  28
  • Elephantik  · 技术社区  · 16 年前

    在我的工作中,我们遇到了一个关于OutofMemoryException的问题。我写了一段简单的代码来模拟一些行为,最后我发现了下面的奥秘。看看这个简单的代码,当它耗尽内存时就会爆炸。

    class Program
    {
        private static void Main()
        {
            List<byte[]> list = new List<byte[]>(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

    GC.收集 在每10次迭代之后,我甚至得到了193716个循环。有两件奇怪的事:

    1. GC.收集 有如此严重的影响(最多多分配30%)?

    4 回复  |  直到 13 年前
        1
  •  11
  •   Bob    16 年前

    垃圾收集过程的一部分是压缩阶段。在这个阶段,分配的内存块被移动以减少碎片。当分配内存时,它并不总是在最后一块分配的内存关闭后分配。所以你可以挤进去一点,因为垃圾收集器通过更好地利用可用空间来腾出更多的空间。

    我试着做一些测试,但我的机器不能处理。试一试,它会告诉你 GC

    byte[] b = new byte[10000];
    GCHandle.Alloc(b, GCHandleType.Pinned);
    list.Add(b);
    

    至于你的评论,当 气相色谱 移动东西,它并没有清除任何东西,它只是更好地利用所有的内存空间。让我们试着把这个简单化。当您第一次分配字节数组时,假设它是从0点到10000点插入到内存中的。下次分配字节数组时,不保证它从10001开始,它可能从10500开始。所以现在有499个字节没有被使用,也不会被应用程序使用。所以当 气相色谱 如果进行压缩,它会将10500数组移动到10001,以便能够使用额外的499字节。再说一遍,这比简化要简单得多。

        2
  •  5
  •   Lucero    16 年前

    根据您使用的CLR,可能会涉及一些大型对象堆问题。

    请看这篇文章,它解释了大块分配的问题(200000个项目的列表肯定是一个大块,另一个可能是,也可能不是,有些数组在达到8k时似乎被放入LOH,其他数组在85k之后)。

    http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

        3
  •  2
  •   Dave Black    8 年前

    CLR偶尔会在LOH上放置数组。如果您通过WinDbg查看内存转储,您将看到有低于85000字节的数组。这是无证行为,但这就是它的工作方式。

    因为正在对LOH堆进行碎片化,并且LOH堆从未被压缩,所以您得到的是OutOfMemoryErrors。

    关于你的问题:

    2) 当没有“丢失”的引用时(我甚至已经预先设置了列表的容量),GC到底能收集什么呢?

    覆盖了对 new byte[10000] 您传递以添加到列表中。编译局部变量并将其分配给 . 对于循环中的每个迭代,都要创建一个预定义大小为10000的新字节[],并将其分配给局部变量。变量的任何前一个值都将被覆盖,并且该内存在下次运行GC以生成变量时(在本例中,可能是LOH)有资格进行收集。

        4
  •  0
  •   NickD    13 年前

    我试了两种方法:

    • 编写自己的堆管理器(使用一个大缓冲区分配内存,只需调整指针)

    如果可能的话,你可以试试.NET 4.5 http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx

    推荐文章