C#性能优化基础:内存诊断(dump)

  接上一篇:C#性能优化基础:垃圾回收机制

  本文说下怎么去查找内存问题,举个例子,我们有这样的一段程序:

    namespace ConsoleApp1
    {
        internal class Program
        {
            static List<Demo> Demos { get; } = new List<Demo>();

            static void Main(string[] args)
            {
                while (true)
                {
                    Console.Write($"请输入要创建的对象个数(已有对象{Demos.Count}个):");
                    var line = Console.ReadLine();
                    if (int.TryParse(line, out var count))
                    {
                        while (count-- > 0)
                        {
                            Demos.Add(new Demo() { Value7 = new Demo[] { new Demo() } });
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
        internal class Demo
        {
            public string Value1 { get; set; } = Guid.NewGuid().ToString("N");
            public string Value2 { get; set; } = Guid.NewGuid().ToString("N").PadRight(100000, '-');
            public int Value3 { get; set; } = new Random().Next(1, 100);
            public DateTime Value4 { get; set; } = DateTime.Now;
            public bool? Value5 { get; set; } = new Random().Next(0, 2) == 1;
            public DayOfWeek Value6 { get; set; } = (DayOfWeek)(new Random().Next(0, 7));
            public Demo[] Value7 { get; set; }
        }
    }

  代码很简单就不说了,我们在linux下去运行这段代码,为什么是linux呢,因为windows的下方法是差不多的,linux命令更方便展示些。

  首先,我们需要安装dotnet,我这边用的.net6,安装在/opt/dotnet目录,更多版本可以去官网下载sdk:https://dotnet.microsoft.com/en-us/download

  接着我们需要配置dotnet的安装目录:

    # 需要在/etc/dotnet/install_location中配置
    sudo mkdir /etc/dotnet
    echo /opt/dotnet > /etc/dotnet/install_location

  如果不配置,后续可能会出现下面的异常:

  image

  然后去官网下载我们需要的工具:

  下载dotnet-counters https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters

  下载dotnet-dumphttps://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-dump

  我们可以直接下载linux下的可执行文件,把它放到dotnet的安装目录即可。

  然后我们把项目运行起来,通过dotnet-counters命令可以查看当前服务器上运行dotnet程序:sudo ./dotnet-counters ps

  image

  我这里采用run命令运行项目的,这样我们就拿到了前面的进程ID(那个demo的项目名)。

  image

  注:有人可能会想,如果只为了拿进程ID,我们可以同topps命令都可以,确实是的,但是那样拿到的命令不一定有用,因为它要求我们的dotnet程序和dotnet-counters在同一个域,比如程序运行在服务单元,但是dotnet-counters运行在shell,那么可能获取不到程序的信息,即上面的命令输出可能没有dotnet进程的信息。

  然后我们还可以通过dotnet-counters命令查看程序进程内部的内存变化:sudo dotnet-counters monitor --refresh-interval=1 -p 361706,结果大概如下图:

  image

  我们关注几个指标就好了:

    GC Heap Size (MB) :堆内存大小
    Gen X GC Count (Count / 1 sec):第X代垃圾数量
    Gen 0 Size (B):第X代垃圾大小
    LOH Size (B):大对象堆的大小

  接下来我们可以通过dotnet-dump来生成转储文件来查看内存中的数据了(转储文件可以理解为此刻内存的一个快照,把它收集到文件里面方便查看):

    # 生成转储文件
    sudo ./dotnet-dump collect -p 361706
    # 分析转储文件,core_20250906_175409是上面生成的转储文件
    sudo ./dotnet-dump analyze core_20250906_175409

  image

  分析查看命令常用的有下面这些(更多命令可以参考官网:https://learn.microsoft.com/en-us/dotnet/framework/tools/sos-dll-sos-debugging-extension

    dumpheap:过滤、统计、打印内存数据信息
        -stat:统计内存数据,一般按照方法表展示(方法表可以理解为就是类型,一个类型的所有数据放在一起就是方法表)
        -mt:指定要展示的方法表
        -min:内存的最小值
        -max:内存的最大值
        -type:指定包含的类型,建议类型的完整形式,包括明明空间,如:System.String
        -live:只列出还活动的内存数据
        -strings:统计字符串数据的信息
        -short:将输出限制为只是每个对象的地址

    dumpobj:显示有关指定地址处的对象的信息。 DumpObj 命令显示对象的字段、EEClass 结构信息、方法表和大小。可简写成do
        -nofields:可阻止显示对象的字段,它对 String 这样的对象很有用。
    
    dumparray:检查数组对象的元素,可简写成da
        -start:选项指定开始显示元素的起始索引。
        -length:选项指定要显示的元素数量。
        -details:选项使用 dumpobj 和 dumpvc 格式显示元素的详细信息。
        -nofields:选项可阻止显示数组。 此选项仅在指定 -details 选项后可用。
        
    dumpvc:显示有关指定地址处的值类字段的信息。
    
    dc:查看字符串数据,对于大字符串比较有效
    
    gcroot:显示有关对指定地址处的对象的引用(或根)的信息

  接下来逐个展示一下他们的用法:

    # 统计每个方法表的对象个数和总内存
    dumpheap -stat
    # 统计每个方法表还活动的大对象的个数和总内存
    dumpheap -stat -min 85000 -live
    # 统计大字符串的个数和内存大小
    dumpheap -strings -min 85000
    # 查看某个方法表的数据
    dumpheap -mt 7f0af83bd2e0
    # 查看某个方法表的大对象数据
    dumpheap -mt 7f0af83bd2e0 -min 85000 -live
    # 查看某个类型的数据
    dumpheap -type System.String

  比如我们查看包含Demo类型的数据:dumpheap -type Demo

  image

  上图说我们Demo类有三个地方:List<Demo>(1个)、Demo[](3个)、Demo对象(2个),它们的地址也在上面列出来了

  我们现在逐个来对应他们的关系,最外层肯定是List<Demo>数组对象,所以我们可以查看一下链表数据(因为链表属于对象,所以用do查看):do 7f72c8008dd0

  image

  可以看到List<Demo>数组对象里面有三个字段,里面的_item字段记录的就是一个Demo[]数组对象(地址7f72c805eb30),这也解释了为什么会有三个数组对象的原因。

  我们接着去看_item字段里面是什么(因为是数组,所以用dumparray):dumparray 00007f72c805eb30

  image

  可以看到这个数组有四个元素,但是只有第一个有数据,其他都是null,我们明明至添加了一个,为什么有四个?读者可自行思考下。

  我们接着看第一个元素里面是什么(数组里面是Demo对象,所以用do查看):do 00007f72c8014ac8

  image

  可以看到对象里面的所有字段,我们可以逐个检查:

  Value3是int数值类型,值是78,Value6是美剧类型,值是4,都已经打印出来了。

  Value1Value2是字符串,可以通过do查看:

  image

  可以看到Value2的结果没有打印出来,因为结果太长了,我们可以换个方式打印,通过dc命令,通过上图得到Value2的长度是100000,那么我们执行:dc -c 100100 -w 100100 00007f72d7fff038 ,这样我们就把所有的字符串都打印出来了。

  Value4DateTime结构体,但是内存里面是一个地址,所以我们不能用do,应该用dumpvc 去查看:dumpvc 00007f72fe0c8668 00007f72c8014af0

  image

  可以看到它只有一个_dateDatalong类型数据,它其实就是DateTimeTick属性。

  Value5Nullable,它是机构体,所以应该通过dumpvc查看:dumpvc 00007f72fe0c8950 00007f72c8014af8

  image

  Value7是数组,我们可以用dumparray查看,这就回到上面的情况了,这里就不解释了

  最后,gcroot也可以演示一下,比如对于Value7,我们执行: gcroot 00007f72c805e948

  image

  它告诉我们地址是怎么关联引用的。

  

  总结

  本文只是介绍怎么去处理内存问题,如果碰到内存过高或者没有释放,我们可以通过dump去查原因。

  当然我们可以通过VS去分析,但是如果是开发环境还好,如果是生产环境,转储文件一般比较大,因为本来就是要处理内存大的原因,而转储文件就是内存的快照,自然就大了,文件一大就不方便搬运。。。

 

posted @ 2025-10-09 18:42  没有星星的夏季  阅读(12)  评论(0)    收藏  举报