代码之家  ›  专栏  ›  技术社区  ›  Paul Ruane

大对象堆碎片

  •  93
  • Paul Ruane  · 技术社区  · 16 年前

    我正在使用的C#/.NET应用程序内存泄漏速度很慢。我曾将CDB与SOS一起使用,试图确定发生了什么,但数据似乎没有任何意义,因此我希望你们中的一位可能曾经经历过这种情况。

    应用程序正在64位框架上运行。它不断地计算数据并将数据序列化到远程主机,并且对大型对象堆(LOH)造成了相当大的影响。但是,我希望大多数LOH对象都是瞬态的:一旦计算完成并发送到远程主机,内存应该被释放。然而,我看到的是大量(活动)对象数组与空闲内存块交错,例如,从LOH中随机抽取一段:

    0:000> !DumpHeap 000000005b5b1000  000000006351da10
             Address               MT     Size
    ...
    000000005d4f92e0 0000064280c7c970 16147872
    000000005e45f880 00000000001661d0  1901752 Free
    000000005e62fd38 00000642788d8ba8     1056       <--
    000000005e630158 00000000001661d0  5988848 Free
    000000005ebe6348 00000642788d8ba8     1056
    000000005ebe6768 00000000001661d0  6481336 Free
    000000005f214d20 00000642788d8ba8     1056
    000000005f215140 00000000001661d0  7346016 Free
    000000005f9168a0 00000642788d8ba8     1056
    000000005f916cc0 00000000001661d0  7611648 Free
    00000000600591c0 00000642788d8ba8     1056
    00000000600595e0 00000000001661d0   264808 Free
    ...
    

    还要注意,CDB在转储堆段时没有报告类型:我不确定这是否相关。如果我转储已标记的(<--)对象,CDB/SOS将报告该对象:

    0:015> !DumpObj 000000005e62fd38
    Name: System.Object[]
    MethodTable: 00000642788d8ba8
    EEClass: 00000642789d7660
    Size: 1056(0x420) bytes
    Array: Rank 1, Number of elements 128, Type CLASS
    Element Type: System.Object
    Fields:
    None
    

    对象数组的元素都是字符串,这些字符串可以从应用程序代码中识别出来。

    此外,我无法找到他们的GC根作为源!GCRoot命令挂起并且再也不会回来(我甚至试着在一夜之间离开它)。


    更新1

    我昨天晚些时候提出的另一个理论是,这些对象数组一开始很大,但已经缩小,留下了内存转储中明显的可用内存块。让我怀疑的是,对象数组总是看起来有1056字节长(128个元素),引用为128*8,开销为32字节。

    其想法是,可能库或CLR中的某些不安全代码正在破坏数组头中的元素数字段。我知道有点遥不可及。。。


    更新2

    多亏了Brian Rasmussen(参见接受的答案),问题被确定为由字符串intern表引起的LOH碎片!我编写了一个快速测试应用程序来确认这一点:

    static void Main()
    {
        const int ITERATIONS = 100000;
    
        for (int index = 0; index < ITERATIONS; ++index)
        {
            string str = "NonInterned" + index;
            Console.Out.WriteLine(str);
        }
    
        Console.Out.WriteLine("Continue.");
        Console.In.ReadLine();
    
        for (int index = 0; index < ITERATIONS; ++index)
        {
            string str = string.Intern("Interned" + index);
            Console.Out.WriteLine(str);
        }
    
        Console.Out.WriteLine("Continue?");
        Console.In.ReadLine();
    }
    

    应用程序首先在循环中创建和取消引用唯一字符串。这只是为了证明在这种情况下内存不会泄漏。显然,它不应该也不应该。

    0:000> .loadby sos mscorwks
    0:000> !EEHeap -gc
    Number of GC Heaps: 1
    generation 0 starts at 0x00f7a9b0
    generation 1 starts at 0x00e79c3c
    generation 2 starts at 0x00b21000
    ephemeral segment allocation context: none
     segment    begin allocated     size
    00b20000 00b21000  010029bc 0x004e19bc(5118396)
    Large object heap starts at 0x01b21000
     segment    begin allocated     size
    01b20000 01b21000  01b8ade0 0x00069de0(433632)
    Total Size  0x54b79c(5552028)
    ------------------------------
    GC Heap Size  0x54b79c(5552028)
    

    转储LOH段显示了我在泄漏应用程序中看到的模式:

    0:000> !DumpHeap 01b21000 01b8ade0
    ...
    01b8a120 793040bc      528
    01b8a330 00175e88       16 Free
    01b8a340 793040bc      528
    01b8a550 00175e88       16 Free
    01b8a560 793040bc      528
    01b8a770 00175e88       16 Free
    01b8a780 793040bc      528
    01b8a990 00175e88       16 Free
    01b8a9a0 793040bc      528
    01b8abb0 00175e88       16 Free
    01b8abc0 793040bc      528
    01b8add0 00175e88       16 Free    total 1568 objects
    Statistics:
          MT    Count    TotalSize Class Name
    00175e88      784        12544      Free
    793040bc      784       421088 System.Object[]
    Total 1568 objects
    

    所以这个故事的寓意是在实习时要非常小心。如果不知道您正在实习的字符串是有限集的成员,那么您的应用程序将由于LOH的碎片而泄漏,至少在CLR的版本2中是这样。

    在我们的应用程序中,在反序列化代码路径中有一些通用代码,在解组过程中会插入实体标识符:我现在强烈怀疑这就是罪魁祸首。然而,开发人员的意图显然是好的,因为他们希望确保如果同一实体被反序列化多次,那么在内存中只保留标识符字符串的一个实例。

    7 回复  |  直到 10 年前
        1
  •  47
  •   Community CDub    8 年前

    CLR使用LOH预先分配一些对象(例如 the array used for interned strings )。其中一些少于85000字节,因此通常不会在LOH上分配。

    同样由于一个有点深奥的优化,任何 double[]

        2
  •  13
  •   Andre Abrantes    10 年前

    NET Framework 4.5.1能够在垃圾收集期间显式压缩大型对象堆(LOH)。

    GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
    GC.Collect();
    

    请参阅中的更多信息 GCSettings.LargeObjectHeapCompactionMode

        3
  •  2
  •   Daniel Earwicker    16 年前

    如果这就是实际发生的情况,那么它将解释小物体如何最终与LOH在同一个地方——如果它们的寿命足够长,可以在第二代中结束。

    因此,你的问题似乎是对我想到的想法的一个很好的反驳——它将导致LOH的分裂。

    能够

    更新: 产量 !dumpheap -stat 这几乎把这个理论从水里吹了出来!第二代和LOH有自己的区域。

        4
  •  1
  •   HUAGHAGUAH    16 年前

    一旦你确定是什么产生了这些字符串,试着找出是什么阻止了它们被GCD。也许它们被塞进了一个被遗忘或未使用的列表中,用于日志记录或类似的目的。


        5
  •  1
  •   Ian Ringrose    15 年前

    好问题,我通过阅读问题来学习。

    考虑到.net垃圾回收器的性能,仅让反序列化代码路径创建普通字符串对象可能就足够了。在证明需要之前,不要做任何更复杂的事情。

    我最多只想保存一个包含您看到的最后几个字符串的哈希表,并重用这些字符串。通过限制哈希表大小并在创建表时传入大小,可以停止大部分碎片。然后需要一种方法从哈希表中删除最近未看到的字符串,以限制其大小。 但是,如果反序列化代码路径创建的字符串无论如何都是短期的,那么您将得不到多少好处。

        6
  •  1
  •   Kevin Goff    15 年前

    这里有两种方法来确定确切的 call-stack LOH 分配。

    为了避免LOH碎片,预先分配大量对象并固定它们。在需要时重用这些对象。这是 post 关于LOH碎片化。类似这样的东西可以帮助避免LOH碎片化。

        7
  •  1
  •   Jose Gzz    8 年前