1. 讲故事摘要:说实话本来是不想写这个系列的,因为我潜意识里觉得这款工具就像美图秀秀一样,拉低专业人士的档次,但奈何在训练营里我需要用到 dottrace 这款工具,而我向官方申请再续了一年免费的Pack套件也给我通过了,所以我觉得要对得起他们,得要写点什么,截图如下:
说实话本来是不想写这个系列的,因为我潜意识里觉得这款工具就像美图秀秀一样,拉低专业人士的档次,但奈何在训练营里我需要用到 dottrace 这款工具,而我向官方申请再续了一年免费的Pack套件也给我通过了,所以我觉得要对得起他们,得要写点什么,截图如下:
internalclassProgram
{
static void Main(string args)
{
for (int i = 1; i 500000; i++)
{
NewPerson(i);
}
Console.WriteLine("50w 个对象插入完毕!");
Console.ReadLine;
}
static void NewPerson(int i)
{
var person = new Person
{
ID = i + 1,
Name = string.Join(",", Enumerable.Range(0, 1000))
};
}
}
publicclassPerson
{
publicint ID { get; set; }
publicstring Name { get; set; }
~Person
{
Thread.Sleep(1000);
Console.WriteLine($"析构函数 {ID}: 执行完毕...");
}
}
2. DotMemory 分析这里我用的是DotMemory 2025.1版本,用 dotmemory 开启子进程的方式启动,大概三步走就行了,截图如下:
稍微熟悉 DotMemory 的朋友,看到快速通览之后肯定会发现问题所在,我就单独开一节来说吧!
3. 问题浮现这个图是告诉大家某一类对象的浅层大小,即不包含他们的孩子节点,用 windbg 的话术就是直接取Person自身的Size=32byte,很显然这32byte是不包含Person.的的,输出如下:0:015> !dumpobj /d 2e9693a2fa0
Name: Example_20_1_1.Person
MethodTable: 00007ffa0b5fa898
EEClass: 00007ffa0b6046a8
Tracked Type: false
Size: 32(0x20) bytes
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa0b4b1188 4000001 10 System.Int32 1 instance 23906
00007ffa0b52ec08 4000002 8 System.String 0 instance 000002e9693a3000
0:015> !DumpObj /d 000002e9693a3000
Name: System.String
MethodTable: 00007ffa0b52ec08
EEClass: 00007ffa0b50a5d8
Tracked Type: false
Size: 7800(0x1e78) bytes
String: 0,1,2,3,...
Fields:
00007ffa0b4b1188 400033b 8 System.Int32 1 instance 3889 _stringLength
00007ffa0b4bb538 400033c c System.Char 1 instance 30 _firstChar
00007ffa0b52ec08 400033a c8 System.String 0 static000002e900000008 Empty
有了上面的思路之后,你应该就知道这个程序中吃的最多的就是String类型,总计3.63G,对 String 产生重大怀疑之后,接下来就是看第二个环形图。
如果说刚才的图是不包含孩子节点的,那这张图就是切切实实的包含孩子节点,有些人可能要问,既然是包含关系,那包含的起点在哪里呢?熟悉 gc标记阶段的朋友应该知道,这个起点应该就是 root 根。
有了这个基础之后,你就应该能明白为什么Person类型的总量是排在第一位的,刚才的 windbg 输出已经告诉了我们,看样子Person.正是我们的问题所在。在内功修炼训练营里跟大家分享过驻留池的底层原理,其实这个就是和驻留池有关,从卦中可以看到由 49.9w 的字符串理应都要进池子,结果都是以副本的形式存在于托管堆中,所以这里有了Wasted=3.63G一说,哈哈,到这里又看到了一处非常不合理的地方,也就说如果把这 49.9w 的string全部进池子,那么内存一下子就下去了,等一会我们来验证吧。这里有一个异常的信号,即,说明这里可能存在一个大问题,从列表中可以看到Person=49.9w,截图如下:这里的49.9w表示什么呢? 熟悉clr终结队列的朋友应该知道,这个 queued 其实就是freachable queue区域,即 终结器线程 提取对象的地方。深绿色区域~Person
{
Thread.Sleep(1000);
Console.WriteLine($"析构函数 {ID}: 执行完毕...");
}
这里稍微提醒一下,在真实场景中,一般会用 windbg 去观察此时的 终结器线程的调用栈,但无奈 dotmemory 不具备观察线程的调用栈能力。
所以解决办法就比较简单了,将 Thread.Sleep(1000);注释掉即可。
最后再说一种办法,也就是刚才说到了 wasted,如果全部送到驻留池,其实也是治标不治本的方法,但在这种场景下可以绝对的延迟OOM的时间,即用 string.Intern给包起来,参考代码如下:
static void NewPerson(int i)
{
var person = new Person
{
ID = i + 1,
Name = string.Intern(string.Join(",", Enumerable.Range(0, 1000)))
};
}
DotMemory虽为美图秀秀,但秀秀也有秀秀的场景,在进一步深度分析之前,它是一款很好的快速通览利器。
来源:opendotnet
