一:配景
1. 讲故事
说真话原来是不想写这个系列的,由于我潜意识里以为这款工具就像美图秀秀一样,拉低专业人士的档次,但怎样在训练营里我必要用到 dottrace 这款工具,而我向官方申请再续了一年免费的Pack套件也给我通过了,以是我以为要对得起他们,得要写点什么,截图如下:
这几天我也过细看了下DotMemory的文档,发现还是有一些可圈可点的地方,毕竟美图秀秀也有美图秀秀的闪光点,在某些场景下完全可以用 DotMemory 作为WinDbg出场的第一套关卡,想来想去我决定还是写5篇托管内存故障来演示下DotMemory的使用,也确实它的可视化做的非常好,那这篇就先从 闭幕队列积蓄 导致的内存暴涨开始吧。
二:内存暴涨分析
1. 标题代码
为了演示 闭幕队列积蓄 引发的内存暴涨,我故意让 闭幕器线程 处置惩罚的慢一些,如许就会存在不停的囤积环境,参考代码如下:- internal class Program
- {
- 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))
- };
- }
- }
- public class Person
- {
- public int ID { get; set; }
- public string Name { get; set; }
- ~Person()
- {
- Thread.Sleep(1000);
- Console.WriteLine($"析构函数 {ID}: 执行完毕...");
- }
- }
复制代码 2. DotMemory 分析
这里我用的是 DotMemory 2025.1 版本,用 dotmemory 开启子历程的方式启动,大概三步走就行了,截图如下:
这里肯定要选择 Sampled 采样模式,假如选择 Full 模式那险些是无法跑的,由于都是基于 ETW 的,以是和 perfview 的 .NET SampleAlloc 模式是千篇同等的。
点击 Start 后,会有一个内存用量的动态图,在内存出现暴涨后,使用 Get Snapshot 采一个快照下来,截图如下:
打开图中左下角的 Snapshot #1 快照,映入眼帘的就是 Inspections 视图,翻译过来用 检测台 比力符合,截图如下:
稍微认识 DotMemory 的朋侪,看到快速通览之后肯定会发现标题地点,我就单独开一节来说吧!
3. 标题浮现
这个图是告诉各人某一类对象的浅层巨细,即不包罗他们的孩子节点,用 windbg 的话术就是直接取Person自身的 Size=32byte,很显然这 32byte 是不包罗 Person.k__BackingField 的 Size=7800byte 的,输出如下:- 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 <ID>k__BackingField
- 00007ffa0b52ec08 4000002 8 System.String 0 instance 000002e9693a3000 <Name>k__BackingField
- 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:
- MT Field Offset Type VT Attr Value Name
- 00007ffa0b4b1188 400033b 8 System.Int32 1 instance 3889 _stringLength
- 00007ffa0b4bb538 400033c c System.Char 1 instance 30 _firstChar
- 00007ffa0b52ec08 400033a c8 System.String 0 static 000002e900000008 Empty
复制代码 有了上面的思绪之后,你应该就知道这个步调中吃的最多的就是String范例,总计 3.63G,对 String 产生庞大猜疑之后,接下来就是看第二个环形图。
- Largest Retained Size 环形图
假如说刚才的图是不包罗孩子节点的,那这张图就是切切实实的包罗孩子节点,有些人大概要问,既然是包罗关系,那包罗的出发点在那边呢?认识 gc标记阶段的朋侪应该知道,这个出发点应该就是 root 根。
有了这个底子之后,你就应该能明白为什么 Person 范例的总量是排在第一位的,刚才的 windbg 输出已经告诉了我们,看样子 Person.k__BackingField 正是我们的标题地点。
在内功修炼训练营里跟各人分享过 驻留池 的底层原理,着实这个就是和 驻留池 有关,从卦中可以看到由 49.9w 的字符串理应都要进池子,效果都是以副本的情势存在于托管堆中,以是这里有了 Wasted=3.63G 一说,哈哈,到这里又看到了一处非常不公道的地方,也就说假如把这 49.9w 的string全部进池子,那么内存一下子就下去了,等一会我们来验证吧。
这里有一个非常的信号,即 赤色感叹号,分析这里大概存在一个大标题,从列表中可以看到 Person=49.9w,截图如下:
这里的 49.9w 表现什么呢? 认识clr闭幕队列的朋侪应该知道,这个 queued 着实就是 freachable queue 地区,即 闭幕器线程 提取对象的地方。
假如有些朋侪还是搞不清楚,在我的训练营里有具体的画图分析,此中的 深绿色地区 就是所谓的提取地区,截图如下:
假如肯定要在 dotmemory 上验证,那就双击呗,观察 Similar Retention 选项即可,截图如下:
言归正传,接下来的标题就来了,为什么 闭幕器队列 中有那么多的囤积?
4. 寻求标题之道
由于是采样模式,直接观察 CallTree 和 Back Traces 选项卡会禁绝,以是就直接观察 Person 的源代码,为什么 析构函数 这么不给力,很快就发现有不对的地方,这里居然有慢处置惩罚 Thread.Sleep(1000),参考如下:- ~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)))
- };
- }
复制代码
从卦中可以看到,着实送入了 50w 的超大 string,由于内存中只保有一份,以是再怎么大也大不起来,从检测台上也能看到那玩意在 String duplicates 列表中消散了,截图如下:
三:总结
DotMemory虽为美图秀秀,但秀秀也有秀秀的场景,在进一步深度分析之前,它是一款很好的快速通览利器。 |