我正在处理的 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 没有报告类型:我不确定这是否相关。如果我转储标记的 (<--) 对象,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 中。
在我们的应用程序的案例中,反序列化代码路径中有通用代码在解组期间实习实体标识符:我现在强烈怀疑这是罪魁祸首。然而,开发人员的意图显然是好的,因为他们想确保如果同一个实体被多次反序列化,那么只有一个标识符字符串的实例将保留在内存中。