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

2024-08-13 18:56:54 浏览数 (4)

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

因为需要获取进程的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集合内存比刚才小很多。

0 人点赞