魏晓东 发表于 2022-9-16 17:18:26

PerfView专题 (第五篇):如何寻找 C# 托管内存泄漏

一:背景

前几篇我们聊的都是 非托管内存泄漏,这一篇我们再看下如何用 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,如下图所示:
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508297-1137072780.png
在真实场景中,你根本不知道是谁占用了这么大的内存,在分析武器库中,用 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
00000000028400000000000002841000000000001283FB1000000000128400000xfffeb10(268430096)0xffff000(268431360)
0000000023E800000000000023E810000000000033E7F0A80000000033E800000xfffe0a8(268427432)0xffff000(268431360)
00000000347D000000000000347D100000000000447CFA9800000000447D00000xfffea98(268429976)0xffff000(268431360)
0000000045A600000000000045A610000000000055A5E2A00000000055A600000xfffd2a0(268423840)0xffff000(268431360)
0000000055A600000000000055A610000000000065A5F7B80000000065A600000xfffe7b8(268429240)0xffff000(268431360)
0000000065A600000000000065A610000000000073252ED800000000735F60000xd7f1ed8(226434776)0xdb95000(230248448)
Large object heap starts at 0x0000000012841000
         segment             begin         allocated         committed    allocated size    committed size
000000001284000000000000128410000000000012C211300000000012C220000x3e0130(4063536)0x3e1000(4067328)
Pinned object heap starts at 0x000000001A841000
000000001A840000000000001A841000000000001A845C38000000001A8520000x4c38(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 拦截。
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508347-29387493.png
需要注意的是,对于这个选项,需要先开启收集,再启动程序,等程序执行完毕后,点击 Stop Collection ,稍等片刻,会看到如下截图。
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508297-735848878.png
点击 GC Heap Net MEM (Coarse Sampling) Stack 列表,选择我们的进程,会看到当前的 System.String 权重占比最高,所以调查它的分配源就是当务之急了,截图如下:
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508279-1419389507.png
接下来双击 System.String 行,查看它的 Callers,逐一往下翻,终于找到了 Program.Alloc1() 方法,截图如下:
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508304-1443312947.png
到这里就找到了问题函数 Alloc1() ,接下来就是探究源码了哈。
3. 生产中可以用 .NET Alloc 吗

现在大家都知道 .NET Alloc 可以实现对象分配拦截,但是在生产场景中,每秒的分配量可能达到几十万,上百万,每一次分配都要拦截,会产生诸多的负面影响。
1) 程序速度变慢。
2) 产生非常大的 zip 文件。
如果你不在意的话,可以这么使用,如果在意,建议用 .NET SampAlloc 选项,它是一种采样的方式,每秒中的同类型分配最多只会采样 100 次,所以在 性能 和 zip文件 两个维度可以达到最优状态。
接下来勾选 .NET SampAlloc 项,其他操作步骤一致,截图如下:
https://img2022.cnblogs.com/blog/214741/202208/214741-20220816100508306-1409349701.png

有点意思的是,观察到的占比都是 43.7% ,
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: PerfView专题 (第五篇):如何寻找 C# 托管内存泄漏