LOH 碎片 - 2015 年更新

LOH fragmentation - 2015 update

有很多关于 .NET LOH 的可用信息,并且已在多篇文章中进行了解释。不过好像有些文章不够精准

信息过时

Brian Rasmussen's answer (2009), program manager at Microsoft, he says the limit is 85000 bytes. He also let's us know that there is an even more curious case of double[] with a size of 1000 elements. The same 85000 limit is stated by Maoni Stephens (MSDN, 2008), member of the CLR team

在评论中,Brian Rasmussen 变得更加准确,让我们知道它可以用 byte[] 85000 字节 - 12 字节来复制。

2013年更新

Mario Hewardt (author of 'Advanced Windows Debugging') 在 2013 年告诉我们,如果我们告诉它这样做,.NET 4.5.1 现在也可以压缩 LOH。由于默认情况下它是关闭的,除非您已经意识到它,否则问题仍然存在。

2015年更新

我无法再重现 byte[] 示例。使用简短的强力算法,我发现我必须减去 24(SOH 中的 byte[84999-24],LOH 中的 byte[85000-24]):

    static void Main(string[] args)
    {
        int diff = 0;
        int generation = 3;
        while (generation > 0)
        {
            diff++;
            byte[] large = new byte[85000-diff];
            generation = GC.GetGeneration(large);
        }            
        Console.WriteLine(diff);
    }

我也无法重现 double[] 语句。暴力破解给了我 10622 个元素作为边界(SOH 中的 double[10621],LOH 中的 double[10622]):

    static void Main(string[] args)
    {
        int size = 85000;
        int step = 85000/2;
        while (step>0)
        {
            double[] d = new double[size];
            int generation = GC.GetGeneration(d);
            size += (generation>0)?-step:step;
            step /= 2;
        }
        Console.WriteLine(size);
    }

即使我为较旧的 .NET 框架编译应用程序,也会发生这种情况。它也不依赖于 Release 或 Debug 版本。

如何解释这些变化?

byte[] 示例中从 12 位到 24 位的变化可以用 CPU 体系结构从 32 位到 64 位的变化来解释。在为 x64 或 AnyCPU 编译的程序中,.NET 开销从 2*4 字节(4 字节对象头 + 4 字节方法 Table)增加到 2*8 字节(8 字节对象头 + 8字节方法 Table)。此外,数组的长度 属性 为 4 个字节(32 位)而不是 8 个字节(64 位)。

double[]为例,用计算器计算一下:85000 bytes / 64 bit for the double type = 10625 items,已经很接近了。考虑到 .NET 开销,结果是(85000 字节 - 24 字节)/每个双精度 8 个字节 = 10622 双精度。所以不再对 double[] 进行特殊处理。

顺便说一句,我之前一直没有找到任何关于LOH分片的工作演示,所以我自己写了一个。只需为 x86 和 运行 编译以下代码。它甚至包括一些调试提示。

编译为 x64 时效果不佳,因为 Windows 可能会增加页面文件的大小,因此后续分配 20 MB 内存可能会再次成功。

class Program
{
    static IList<byte[]> small = new List<byte[]>();
    static IList<byte[]> big = new List<byte[]>(); 

    static void Main()
    {
        int totalMB = 0;
        try
        {
            Console.WriteLine("Allocating memory...");
            while (true)
            {
                big.Add(new byte[10*1024*1024]);
                small.Add(new byte[85000-3*IntPtr.Size]);
                totalMB += 10;
                Console.WriteLine("{0} MB allocated", totalMB);
            }
        }
        catch (OutOfMemoryException)
        {
            Console.WriteLine("Memory is full now. Attach and debug if you like. Press Enter when done.");
            Console.WriteLine("For WinDbg, try `!address -summary` and  `!dumpheap -stat`.");
            Console.ReadLine();

            big.Clear();
            GC.Collect();
            Console.WriteLine("Lots of memory has been freed. Check again with the same commands.");
            Console.ReadLine();

            try
            {
                big.Add(new byte[20*1024*1024]);
            }
            catch(OutOfMemoryException)
            {
                Console.WriteLine("It was not possible to allocate 20 MB although {0} MB are free.", totalMB);
                Console.ReadLine();
            }
        }
    }
}