该应用程序在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();
}
该应用程序首先在循环中创建并取消引用唯一的字符串。这只是为了证明在这种情况下内存不会泄漏。显然,内存不应该泄漏,它确实没有。
在第二个循环中,唯一的字符串被创建并国际化。这个操作将它们固定在intern表中。我没有意识到的是intern表是如何表示的。它似乎由一组页面组成——每个页面包含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
需要注意的是,由于我的工作站是32位的,应用程序服务器是64位的,因此对象数组大小为528(而不是1056)。然而,对象数组仍然有128个元素。
因此,这个故事告诉我们在使用interning时要非常小心。如果你intern的字符串不属于有限集合的成员,那么你的应用程序将因为LOH的碎片化而泄漏内存,至少在CLR版本2中会出现这种情况。
在我们应用程序的情况下,在反序列化代码路径中有一般代码来intern实体标识符:我现在强烈怀疑这就是罪魁祸首。但是,开发人员的意图显然是好的,他们想要确保如果同一个实体被反序列化多次,那么只有一个标识符字符串实例会被维护在内存中。