C#/.我正在开发的NET应用程序内存泄漏缓慢.我曾使用CDB和SOS来try 确定发生了什么,但数据似乎没有任何意义,所以我希望你们中的一位之前可能经历过这种情况.

该应用程序在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命令挂起,再也不会回来(我甚至试着在一夜之间离开它).

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


更新1

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

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


更新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();
}

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

在第二个循环中,创建并插入唯一的字符串.这一行动让他们在实习生桌上生根发芽.我没有意识到实习生表是如何呈现的.它似乎由一组在LOH中创建的页面(128个字符串元素的对象数组)组成.这在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),因为我的 workstation 是32位的,而应用服务器是64位的.对象数组仍然有128个元素长.

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

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

推荐答案

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

这是一个实现细节,但我认为这样做的原因是为了避免不必要的垃圾收集实例,这些实例应该在进程本身的过程中一直存在.

此外,由于一个有点深奥的优化,double[]0个或更多元素中的任何double[]个也会分配到LOH上.

.net相关问答推荐

StackExchange.Redis和NRedisStack包有什么不同?

在 .NET 中使用 AES 解密时缺少后半字节

如何正确使用await using语法?

如何判断属性设置器是否公开

移位比Java中的乘法和除法更快吗? .网?

如何使用 Moq 为不同的参数设置两次方法

如何获得友好的操作系统版本名称?

NonSerialized 属性

在 web api 控制器(.net 核心)中使用 async/await 或任务

我可以从我的应用程序中抛出哪些内置 .NET 异常?

在 WPF 窗口中获取当前聚焦的元素/控件

如何在 C# 中创建 Word 文档?

托管和非托管代码、内存和大小有什么区别?

C# 中 try/finally 的开销?

什么决定了 Path.GetTempPath() 的返回值?

多个等待与 Task.WaitAll - 等效?

在 C# 中与块等效?

公钥令牌的作用是什么?

Environment.GetFolderPath(...CommonApplicationData) 在 Vista 上仍然返回C:\Documents and Settings\

实体框架太慢了.我有哪些 Select ?