一:背景
前几篇我们聊的都是 非托管内存泄漏,这一篇我们再看下如何用 PerfView 来排查 托管内存泄漏 ,其实 托管内存泄漏 比较好排查,尤其是用 WinDbg,毕竟C#是带有丰富的元数据,不像C++下去就是二进制。
二:如何分析
PerfView 用的是权重占比来寻找可疑的问题函数,为了方便讲述,我们先上一段问题代码。- internal class Program
- {
- static void Main(string[] args)
- {
- Task.Run(Alloc1);
- Task.Run(Alloc2);
- Task.Run(Alloc3);
- Console.ReadLine();
- }
- static void Alloc1()
- {
- var list = new List<string>();
- for (int i = 0; i < 200000; i++)
- {
- list.Add(string.Join(",", Enumerable.Range(0, 1000)));
- }
- Console.WriteLine("Alloc1 处理完毕");
- }
- static void Alloc2()
- {
- var list = new List<string>();
- for (int i = 0; i < 100; i++)
- {
- list.Add(string.Join(",", Enumerable.Range(0, 1000)));
- }
- Console.WriteLine("Alloc2 处理完毕");
- }
- static void Alloc3()
- {
- var list = new List<string>();
- for (int i = 0; i < 100; i++)
- {
- list.Add(string.Join(",", Enumerable.Range(0, 1000)));
- }
- Console.WriteLine("Alloc3 处理完毕");
- }
- }
复制代码 这段代码运行完成后会发现内存占用高达 1.5G,如下图所示:

在真实场景中,你根本不知道是谁占用了这么大的内存,在分析武器库中,用 WinDbg 肯定是最稳的,既然是介绍 PerfView 工具,得用它来分析。
二:PerfView 分析
1. 到底是哪里的泄漏
分析之前,还是要先搞清楚到底是哪里的泄漏,才好用 PerfView 追查下来,首先用 !eeheap -gc 查看下托管堆的占用大小。- 0:005> !eeheap -gc
- Number of GC Heaps: 1
- generation 0 starts at 0x0000000072D7AEC0
- generation 1 starts at 0x0000000072B1B790
- generation 2 starts at 0x0000000002841000
- ephemeral segment allocation context: none
- segment begin allocated committed allocated size committed size
- 0000000002840000 0000000002841000 000000001283FB10 0000000012840000 0xfffeb10(268430096) 0xffff000(268431360)
- 0000000023E80000 0000000023E81000 0000000033E7F0A8 0000000033E80000 0xfffe0a8(268427432) 0xffff000(268431360)
- 00000000347D0000 00000000347D1000 00000000447CFA98 00000000447D0000 0xfffea98(268429976) 0xffff000(268431360)
- 0000000045A60000 0000000045A61000 0000000055A5E2A0 0000000055A60000 0xfffd2a0(268423840) 0xffff000(268431360)
- 0000000055A60000 0000000055A61000 0000000065A5F7B8 0000000065A60000 0xfffe7b8(268429240) 0xffff000(268431360)
- 0000000065A60000 0000000065A61000 0000000073252ED8 00000000735F6000 0xd7f1ed8(226434776) 0xdb95000(230248448)
- Large object heap starts at 0x0000000012841000
- segment begin allocated committed allocated size committed size
- 0000000012840000 0000000012841000 0000000012C21130 0000000012C22000 0x3e0130(4063536) 0x3e1000(4067328)
- Pinned object heap starts at 0x000000001A841000
- 000000001A840000 000000001A841000 000000001A845C38 000000001A852000 0x4c38(19512) 0x11000(69632)
- Total Allocated Size: Size: 0x5dbcdce8 (1572658408) bytes.
- Total Committed Size: Size: 0x5df71000 (1576472576) bytes.
- ------------------------------
- GC Allocated Heap Size: Size: 0x5dbcdce8 (1572658408) bytes.
- GC Committed Heap Size: Size: 0x5df71000 (1576472576) bytes.
复制代码 从输出中可以看到,当前的 托管堆 占用 1.5G, 这就说明当前的泄漏确实是 托管堆 的泄漏,这就给继续分析指明了方向。
2. 使用 .NET Alloc 拦截
在 PerfView 中有一个 .NET Alloc 选项,它可以拦截每一次对象分配,然后记录下 线程调用栈,再根据分配量计算权重,知道原理后,接下来就可以开启 .NET Alloc 拦截。

需要注意的是,对于这个选项,需要先开启收集,再启动程序,等程序执行完毕后,点击 Stop Collection ,稍等片刻,会看到如下截图。

点击 GC Heap Net MEM (Coarse Sampling) Stack 列表,选择我们的进程,会看到当前的 System.String 权重占比最高,所以调查它的分配源就是当务之急了,截图如下:

接下来双击 System.String 行,查看它的 Callers,逐一往下翻,终于找到了 Program.Alloc1() 方法,截图如下:

到这里就找到了问题函数 Alloc1() ,接下来就是探究源码了哈。
3. 生产中可以用 .NET Alloc 吗
现在大家都知道 .NET Alloc 可以实现对象分配拦截,但是在生产场景中,每秒的分配量可能达到几十万,上百万,每一次分配都要拦截,会产生诸多的负面影响。
1) 程序速度变慢。
2) 产生非常大的 zip 文件。
如果你不在意的话,可以这么使用,如果在意,建议用 .NET SampAlloc 选项,它是一种采样的方式,每秒中的同类型分配最多只会采样 100 次,所以在 性能 和 zip文件 两个维度可以达到最优状态。
接下来勾选 .NET SampAlloc 项,其他操作步骤一致,截图如下:

有点意思的是,观察到的占比都是 43.7% ,
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |