前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【.NET】通过代码实现导出进程的dump文件和内存分析

【.NET】通过代码实现导出进程的dump文件和内存分析

作者头像
Wesky
发布2024-08-13 18:56:54
1500
发布2024-08-13 18:56:54
举报
文章被收录于专栏:Dotnet Dancer

前言:没啥可写的,详情直接看下文:

因为需要获取进程的processID,所以接着上次写的识别.NET进程的控制台程序【参考检测.NET CORE+和.NET FX进程有关那个文章】,直接在这上面新增功能。

当前引用的包如下:

先根据ProcessID,导出进程的dump文件。例如自动导出到桌面,并根据当前时间命名:

代码语言:javascript
复制
var client = new DiagnosticsClient(processId);
            string dumpFileName = $"heapdump_{DateTime.Now.ToString("yyyyMMddHHmmss")}.dmp";
            string fullPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), dumpFileName);

此处使用的是.NET 6环境,所以默认情况下可以无损导出.NET6 进程的dump文件。但是不排除有.NET CORE或其他版本环境,有可能不兼容。所以还可以通过dotnel-dump工具来导出。

编写验证是否本地有dump环境的代码,会通过命令行的形式进行验证:

代码语言:javascript
复制
 
代码语言:javascript
复制
bool IsDotnetDumpInstalled()
        {
            try
            {
                var process = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "dotnet-dump",
                        Arguments = "--version",
                        RedirectStandardOutput = true,
                        UseShellExecute = false,
                        CreateNoWindow = true,
                    }
                };

                process.Start();
                string output = process.StandardOutput.ReadToEnd();
                process.WaitForExit();

                return !string.IsNullOrEmpty(output);
            }
            catch
            {
                return false;
            }
        }

以上进行dump工具的验证,如果不存在,则通过命令行安装一下这个工具:

代码语言:javascript
复制
void InstallDotnetDump()
        {
            try
            {
                var process = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "dotnet",
                        Arguments = "tool install --global dotnet-dump",
                        UseShellExecute = false,
                        CreateNoWindow = true,
                    }
                };

                process.Start();
                process.WaitForExit();

                Console.WriteLine("`dotnet-dump` 安装成功.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"安装`dotnet-dump` 失败: {ex.Message}");
            }
        }

通过dump导出工具进行导出指定的进程ID的程序到指定文件路径:

代码语言:javascript
复制
 void UseExternalTool(int processId, string fullPath)
        {
            var startInfo = new ProcessStartInfo
            {
                FileName = "dotnet-dump",
                Arguments = $"collect -p {processId} -o {fullPath}",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using var process = Process.Start(startInfo);
            process.WaitForExit();
        }

如果能够确定要导出dump的进程和当前运行程序是同样的.NET环境,则可以使用DiagnosticsClient的实例直接导出。如果不是会报错,则进行dump工具进行导出。直接导出:

代码语言:javascript
复制
client.WriteDump(DumpType.Full, fullPath);

再进一步,来解析出所有的类型,并打印出该类型的内存占用(非具体对象占用,仅是类型本身占用).需要引入刚才导出的dump文件路径:

代码语言:javascript
复制
 
代码语言:javascript
复制
using (DataTarget dataTarget = DataTarget.LoadDump(dumpFilePath))
            {
                ClrInfo clrInfo = dataTarget.ClrVersions[0];
                ClrRuntime runtime = clrInfo.CreateRuntime();

                ClrHeap heap = runtime.Heap;
                Dictionary<string, ulong> typeSizes = new Dictionary<string, ulong>();
                List<string> typeNames = new List<string>();

                foreach (ClrObject obj in heap.EnumerateObjects())
                {
                    ClrType type = obj.Type;
                    if (type != null)
                    {
                        if (!typeSizes.ContainsKey(type.Name))
                        {
                            typeSizes[type.Name] = 0;
                        }
                        typeSizes[type.Name] += obj.Size;
                    }
                }
                var sortedTypeSizes = typeSizes.OrderBy(entry => entry.Key).ToList();
                foreach (var entry in sortedTypeSizes)
                {
                    Console.WriteLine($"{entry.Key}: {entry.Value} bytes");
                }
            }

新建一个测试用的控制台,此处为了区分效果,我创建的是.net core3.1的控制台:

并且新增一个类型,用来测试看是否可以被程序识别到它的类型:

代码语言:javascript
复制
public class TestClass
    {
        private int loop = 0;
        public string loopstr = "";
        private List<string> strList = null;
       

        public void Test()
        {
            List<int> ints = new List<int>();
            strList = new List<string>();
            for (int i = 0; i <= 100000; i++)
            {
                Console.WriteLine(i);
                loop++;
                loopstr = i.ToString();
                Thread.Sleep(500);
                ints.Add(i);
                strList.Add(loopstr);
            }
        }

    }

在启动项里面调用:

然后先启动这个测试用的程序:

运行上面之前获取.NET进程和ID的程序,获取下刚才程序的ID,此处是781144

接下来为了方便,直接手动写死该ID,来进行接下来的实验。

新建了一个Tracing方法,用来包容上面写的导出dump和统计类型有关:

把上面的进程ID直接传进来,看下效果:

运行控制台程序,输出另一个控制台程序的所有类型,以及定义内存信息:

同时,也可以看到桌面上多了一个导出的dump文件,该文件也可以拿去给专门的dump分析工具进行分析

当然,我们也可以自己分析,例如分析所有的属性、全局变量的内存占用情况。

由于同一个属性,可能会有多处使用,所以做个递归,用来累积属性的大小:

代码语言:javascript
复制
 private ulong CalculateSize(ClrValueType valueType, HashSet<ulong> visitedObjects)
        {
            if (visitedObjects.Contains(valueType.Address))
                return 0;

            visitedObjects.Add(valueType.Address);
            ulong size = 0;
            try
            {
                size = valueType.Size;
            }
            catch (Exception)
            {
                return 0;
            }

            foreach (ClrInstanceField field in valueType.Type.Fields)
            {
                if (field.IsObjectReference)
                {
                    ClrObject fieldValue = field.ReadObject(valueType.Address, interior: true);
                    size += CalculateSize(fieldValue, visitedObjects);
                }
                else if (field.IsValueType)
                {
                    ClrValueType fieldValueStruct = field.ReadStruct(valueType.Address, interior: true);
                    size += CalculateSize(fieldValueStruct, visitedObjects);
                }
            }

            return size;
        }
代码语言:javascript
复制
 
代码语言:javascript
复制
private ulong CalculateSize(ClrObject obj, HashSet<ulong> visitedObjects)
        {
            if (obj.IsNull || visitedObjects.Contains(obj.Address))
                return 0;

            visitedObjects.Add(obj.Address);
            ulong size = 0;
            try
            {
                size = obj.Size;
            }
            catch (Exception)
            {
                return 0;
            }

            foreach (ClrInstanceField field in obj.Type.Fields)
            {
                if (field.IsObjectReference)
                {
                    ClrObject fieldValue = field.ReadObject(obj.Address, interior: false);
                    size += CalculateSize(fieldValue, visitedObjects);
                }
                else if (field.IsValueType)
                {
                    ClrValueType fieldValueStruct = field.ReadStruct(obj.Address, interior: false);
                    size += CalculateSize(fieldValueStruct, visitedObjects);
                }
            }

            return size;
        }

根据指定的类型名称,以及dump文件路径,编写统计属性或全局变量的内存占用方法:

代码语言:javascript
复制
 using DataTarget dataTarget = DataTarget.LoadDump(dumpFilePath);
            ClrInfo runtimeInfo = dataTarget.ClrVersions[0];
            ClrRuntime runtime = runtimeInfo.CreateRuntime();

            ClrHeap heap = runtime.Heap;

            foreach (ClrObject obj in heap.EnumerateObjects())
            {
                if (obj.Type?.Name == targetTypeName)
                {
                    foreach (ClrInstanceField field in obj.Type.Fields)
                    {
                        string fieldName = field.Name;
                        string fieldType = field.Type?.Name ?? "Unknown";
                        ulong fieldSize = 0;


                        if (field.IsObjectReference)
                        {
                            ClrObject fieldValueObj = field.ReadObject(obj.Address, interior: false);
                            fieldSize = CalculateSize(fieldValueObj, new HashSet<ulong>());
                        }
                        else if (field.IsValueType)
                        {
                            ClrValueType fieldValueStruct = field.ReadStruct(obj.Address, interior: false);
                            fieldSize = CalculateSize(fieldValueStruct, new HashSet<ulong>());
                        }
                        else if (field.IsPrimitive)
                        {
                            fieldSize = (ulong)(field.Type?.StaticSize ?? 0);
                        }

                          Console.WriteLine($"父类:{targetTypeName}, 属性名称: {fieldName}, 属性类型: {fieldType},  属性大小: {fieldSize} bytes");
                    }

                        Console.WriteLine("--------------------------------------------");

                }
            }

再根据以上获取的类型,直接传入参数进行测试:

运行程序,查看效果,可以看到由于List集合一直在累积增加,所以内存占用比较大。如果程序一直运行,后续也会继续越来越大。

例如我按Ctrl C关闭进程,然后重新启动,获取到当前测试的进程ID是 785996 重新执行

获取到当前输出的内存大小,List集合内存比刚才小很多。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Dotnet Dancer 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档