c# - 大对象堆碎片

我正在研究的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
...

显然,如果我的应用程序在每次计算期间创建长寿命的大对象,我会期望这种情况。 (它确实这样做,我接受会有一定程度的LOH碎片,但这不是问题。)问题是你可以在上面的转储中看到的非常小的(1056字节)对象数组,我在代码中看不到 正在创建,并以某种方式保持根深蒂固。

另请注意,转储堆段时CDB不报告类型:我不确定这是否相关。 如果我转储标记的(&lt; - )对象,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命令挂起并且永远不会回来(我甚至尝试过夜)。

所以,如果有人能够解释为什么这些小的(<85k)对象数组最终会出现在LOH上,我会非常感激:.NET会在哪些情况下放置一个小的对象数组? 此外,是否有人碰巧知道确定这些物体根源的另一种方法?


更新1

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

这个想法是,库或CLR中的一些不安全的代码可能会破坏数组头中的元素数量字段。 我知道一点远射...


更新2

感谢Brian Rasmussen(见接受的答案),问题已被确定为由字符串实习生表引起的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();
}

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

在第二个循环中,创建并实现唯一字符串。 这个动作将它们植根于实习生表中。 我没有意识到的是如何表示实习生表。 它似乎由一组页面组成 - 一组128个字符串元素 - 在LOH中创建。 这在CDB / SOS中更为明显:

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

请注意,对象数组大小为528(而不是1056),因为我的工作站是32位,应用程序服务器是64位。 对象数组仍然是128个元素。

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

在我们的应用程序的情况下,反序列化代码路径中有一些通用代码在解组期间实体实体标识符:我现在强烈怀疑这是罪魁祸首。 但是,开发人员的意图显然很好,因为他们想确保如果同一实体多次反序列化,那么只有一个标识符字符串实例将保留在内存中。

7个解决方案
42 votes

CLR使用LOH预分配一些对象(例如用于实习字符串的数组)。 其中一些小于85000字节,因此通常不会在LOH上分配。

这是一个实现细节,但我认为这样做的原因是为了避免不必要的垃圾收集实例,只要它自己进程就应该存活。

此外,由于有些深奥的优化,在LOH上也分配了1000个或更多元素的任何double[]

Brian Rasmussen answered 2019-08-12T21:26:18Z
12 votes

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

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

请在GCSettings.LargeObjectHeapCompactionMode中查看更多信息

Andre Abrantes answered 2019-08-12T21:26:50Z
2 votes

当阅读关于GC如何工作的描述,以及关于长寿对象如何在第2代中结束的部分,以及LOH对象的集合仅在完全收集时发生 - 正如第2代的集合一样,脑海中浮现的想法是。 ..为什么不将第2代和大型物品放在同一堆中,因为它们会被收集在一起?

如果那是实际发生的事情,那么它将解释小物体如何与LOH在同一个地方结束 - 如果它们长期存活到足以在第2代结束。

所以你的问题似乎是对我发生的想法的一个很好的反驳 - 它会导致LOH的碎片化。

总结:您的问题可以解释为LOH和第2代共享相同的堆区域,尽管这并不能证明这是解释。

更新:!dumpheap -stat的输出几乎把这个理论吹出了水! 第2代和LOH有自己的区域。

Daniel Earwicker answered 2019-08-12T21:27:43Z
1 votes

如果格式可以识别为您的应用程序,为什么还没有确定生成此字符串格式的代码? 如果有多种可能性,请尝试添加唯一数据以确定哪个代码路径是罪魁祸首。

数组与大量释放项目交错的事实让我猜测它们最初是配对的,或者至少是相关的。 尝试识别释放的对象,以确定生成它们的内容以及关联的字符串。

一旦确定了生成这些字符串的内容,请尝试找出阻止它们进行GCed的内容。 也许他们被遗忘在一个被遗忘或未使用的列表中用于记录目的或类似的东西。


编辑:暂时忽略内存区域和特定的数组大小:只需弄清楚这些字符串的作用是什么导致泄漏。 当程序创建或操作这些字符串一次或两次时,尝试使用!GCRoot,此时要跟踪的对象较少。

HUAGHAGUAH answered 2019-08-12T21:28:30Z
1 votes

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

我认为反序列化代码路径的其他部分也使用大对象堆,因此碎片化。 如果所有的字符串都是在同一时间实习的话,我想你会没事的。

鉴于.net垃圾收集器有多好,只要让反序列化代码路径创建正常的字符串对象就足够了。 在需要得到证实之前,不要做任何更复杂的事情。

我最多会考虑保留你看过的最后几个字符串的哈希表并重用这些字符串。 通过限制哈希表大小并在创建表时传递大小,可以停止大多数碎片。 然后,您需要一种方法来从哈希表中删除最近没有看到的字符串,以限制它的大小。 但是,如果反序列化代码路径创建的字符串无论如何都是短暂的,如果有的话,你将获得很多。

Ian Ringrose answered 2019-08-12T21:29:17Z
1 votes

以下是识别LOH分配的确切调用堆栈的几种方法。

并避免LOH碎片预先分配大量对象并固定它们。 需要时重用这些对象。 这是关于LOH Fragmentation的帖子。 这样的事情可以帮助避免LOH碎片化。

Naveen answered 2019-08-12T21:29:49Z
1 votes

根据[http://webcache.googleusercontent.com/search?q=cache:l3Ppy_eFoAAJ:www.wintellect.com/devcenter/sloscialo/hey-who-stole-all-my-memory+&amp;cd=5&amp;hl= EN&安培; CT = clnk&安培; GL= MX&安培;客户=火狐-b]

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Jose Gzz answered 2019-08-12T21:30:13Z
translate from https://stackoverflow.com:/questions/686950/large-object-heap-fragmentation