前言:没啥可写的,详情直接看下文:
因为需要获取进程的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集合内存比刚才小很多。