接上一篇: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/dotnetecho /opt/dotnet > /etc/dotnet/install_location
如果不配置,后续可能会出现下面的异常:
然后去官网下载我们需要的工具:
下载dotnet-counters
:https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters
下载dotnet-dump
:https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-dump
我们可以直接下载linux下的可执行文件,把它放到dotnet
的安装目录即可。
然后我们把项目运行起来,通过dotnet-counters
命令可以查看当前服务器上运行dotnet程序:sudo ./dotnet-counters ps
我这里采用run命令运行项目的,这样我们就拿到了前面的进程ID(那个demo的项目名)。
注:有人可能会想,如果只为了拿进程ID,我们可以同top
、ps
命令都可以,确实是的,但是那样拿到的命令不一定有用,因为它要求我们的dotnet
程序和dotnet-counters
在同一个域,比如程序运行在服务单元,但是dotnet-counters
运行在shell,那么可能获取不到程序的信息,即上面的命令输出可能没有dotnet
进程的信息。
然后我们还可以通过dotnet-counters
命令查看程序进程内部的内存变化:sudo dotnet-counters monitor --refresh-interval=1 -p 361706
,结果大概如下图:
我们关注几个指标就好了:
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
分析查看命令常用的有下面这些(更多命令可以参考官网: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
上图说我们Demo类有三个地方:List<Demo>
(1个)、Demo[]
(3个)、Demo
对象(2个),它们的地址也在上面列出来了
我们现在逐个来对应他们的关系,最外层肯定是List<Demo>
数组对象,所以我们可以查看一下链表数据(因为链表属于对象,所以用do
查看):do 7f72c8008dd0
可以看到List<Demo>
数组对象里面有三个字段,里面的_item
字段记录的就是一个Demo[]
数组对象(地址7f72c805eb30),这也解释了为什么会有三个数组对象的原因。
我们接着去看_item
字段里面是什么(因为是数组,所以用dumparray
):dumparray 00007f72c805eb30
可以看到这个数组有四个元素,但是只有第一个有数据,其他都是null,我们明明至添加了一个,为什么有四个?读者可自行思考下。
我们接着看第一个元素里面是什么(数组里面是Demo
对象,所以用do
查看):do 00007f72c8014ac8
可以看到对象里面的所有字段,我们可以逐个检查:
Value3
是int数值类型,值是78,Value6
是美剧类型,值是4,都已经打印出来了。
Value1
和Value2
是字符串,可以通过do
查看:
可以看到Value2
的结果没有打印出来,因为结果太长了,我们可以换个方式打印,通过dc
命令,通过上图得到Value2
的长度是100000,那么我们执行:dc -c 100100 -w 100100 00007f72d7fff038
,这样我们就把所有的字符串都打印出来了。
Value4
是DateTime
结构体,但是内存里面是一个地址,所以我们不能用do
,应该用dumpvc
去查看:dumpvc 00007f72fe0c8668 00007f72c8014af0
可以看到它只有一个_dateData
的long
类型数据,它其实就是DateTime
的Tick
属性。
Value5
是Nullable
,它是机构体,所以应该通过dumpvc
查看:dumpvc 00007f72fe0c8950 00007f72c8014af8
Value7
是数组,我们可以用dumparray
查看,这就回到上面的情况了,这里就不解释了
最后,gcroot
也可以演示一下,比如对于Value7
,我们执行: gcroot 00007f72c805e948
它告诉我们地址是怎么关联引用的。
总结
本文只是介绍怎么去处理内存问题,如果碰到内存过高或者没有释放,我们可以通过dump去查原因。
当然我们可以通过VS去分析,但是如果是开发环境还好,如果是生产环境,转储文件一般比较大,因为本来就是要处理内存大的原因,而转储文件就是内存的快照,自然就大了,文件一大就不方便搬运。。。