找回密码
 立即注册
首页 业界区 业界 DotTrace系列:7. 诊断 托管和非托管 内存暴涨 ...

DotTrace系列:7. 诊断 托管和非托管 内存暴涨

愆蟠唉 2025-6-29 10:27:03
一:背景

1. 讲故事

分析托管和非托管内存暴涨,很多人潜意识里都会想到抓dump上windbg分析,但我说可以用dottrace同样分析出来,是不是听起来有点让人惊讶,哈哈,其实很正常,它是另辟蹊径采用底层的ETW机制,即开启 windows 底层日志,所以 dottrace 可以做,官方血统的 perfview 就更可以了,话不多说,这篇就来开干吧。
二:托管和非托管内存分析

1. 托管内存暴涨

用 windbg 分析的话,基本上就是 !eeheap -gc +  !dumpheap -stat + !gcroot 三板斧搞定,但dump的分析方式也不全是优点,它最大的缺点就是dump>20G 时,windbg 基本上就分析不动了,这个很致命,而且 >20G 的dump在分发方面也很麻烦,费时费力,所以在这种情况下,可以借助摄像头dottrace来解决此类问题。
比如有这样的一个场景:我有一个程序平时都是好好的,最近修复了一个bug,上线之后不知道为什么就吃了 4.4G+的内存,这明显是超出预期的,现在很惶恐,截图如下:
1.png

我用 vmmap 简单看了下发现主要是 托管堆 的泄露,截图如下:
2.png

由于dump是非常保密的,不适合分发给第三方,在生产上搭建windbg工作台也不是很方便,有没有轻量级的工具直接分析呢?哈哈,这时候就可以考虑下 dotrrace,它可以帮你找出托管内存分配都是从哪个方法出来的。
使用 dotrace 初始化跟踪或者附加进程一段时间后,采集到的跟踪文件如下:
3.png

从 Filters 面板中可以看到有一个 .NET Memory Alocations 项,上面记录着当前程序分配的内存总量,接下来就可以选中进行下钻分析,截图如下:
4.png

从卦中可以清晰的看到如下信息:

  • 托管内存主要被 LOH 大对象堆给吃了
  • 托管堆上最多的对象是 System.Byte[]
看到这里心里就踏实多了,接下来选中 System.Byte[],看下这些分配都藏在哪些方法里,接下来选择 Hotspots 中的 Plain List 选项,截图如下:
5.png

从卦中可以看到内存主要被 LoadCustomerAttachments 方法给吃掉了,接下来点击 Show Code 观察该方法源码,代码参考如下:
  1.         static void LoadCustomerAttachments()
  2.         {
  3.             Console.WriteLine($"[客户附件] 开始加载 (线程ID: {Thread.CurrentThread.ManagedThreadId})");
  4.             try
  5.             {
  6.                 var attachments = new Dictionary<int, byte[]>();
  7.                 for (int i = 0; i < 30; i++)
  8.                 {
  9.                     attachments[i] = new byte[100 * 1024 * 1024];
  10.                     for (int j = 0; j < attachments[i].Length; j += 1024)
  11.                     {
  12.                         attachments[i][j] = (byte)(i + j);
  13.                     }
  14.                     // 每5个附件输出一次进度
  15.                     if (i % 5 == 0)
  16.                     {
  17.                         Console.WriteLine($"[客户附件] 已加载 {i} 个附件 ({(i + 1) * 100}MB)");
  18.                     }
  19.                 }
  20.                 Console.WriteLine($"[客户附件] 加载完成,共{attachments.Count}个附件");
  21.             }
  22.             catch (OutOfMemoryException ex)
  23.             {
  24.                 Console.WriteLine($"[客户附件] 内存不足错误: {ex.Message}");
  25.             }
  26.         }
复制代码
到这里基本就真相大白,是不是有点像 ust 效果。
2. 非托管内存暴涨

不管是 linux 还是 windows,分析非托管内存泄露都是一个很苟的活,如果非托管内存的泄露是在 ntheap 上,除了重量级的 dump 分析之后,还可以使用轻量级的 dottrace,你没听错,dottrace 是可以分析 ntheap 堆泄露,前提就是勾选上 Collect only unreleased allcations,其实本质也是借助底层的 ETW 事件,截图如下:
6.png

为了方便演示,我用 C# 调用 C++ 来实现一个NTHEAP 的非托管内存泄露,然后借助 dottrace 快速分析,首先定义几个C 导出函数,代码如下:
  1. extern "C"
  2. {
  3.         _declspec(dllexport) void HeapMalloc1(int bytes);
  4.         _declspec(dllexport) void HeapMalloc2(int bytes);
  5.         _declspec(dllexport) void HeapMalloc3(int bytes);
  6. }
  7. #include "iostream"
  8. #include <Windows.h>
  9. using namespace std;
  10. void HeapMalloc1(int bytes)
  11. {
  12.         int* ptr = (int*)malloc(bytes);
  13.         printf("bytes=%d ,分配完毕\n", bytes);
  14. }
  15. void HeapMalloc2(int bytes)
  16. {
  17.         int* ptr = (int*)malloc(bytes);
  18.         printf("bytes=%d ,分配完毕\n", bytes);
  19. }
  20. void HeapMalloc3(int bytes)
  21. {
  22.         int* ptr = (int*)malloc(bytes);
  23.         printf("bytes=%d ,分配完毕\n", bytes);
  24. }
复制代码
接下来通过C#不断的调用这几个函数,其中 HeapMalloc1 方法会泄露 2G 的内存,参考代码如下:
  1. namespace MemoryLeakSimulator
  2. {
  3.     internal class Program
  4.     {
  5.         [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
  6.         extern static void HeapMalloc1(int bytes);
  7.         [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
  8.         extern static void HeapMalloc2(int bytes);
  9.         [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
  10.         extern static void HeapMalloc3(int bytes);
  11.         static void Main(string[] args)
  12.         {
  13.             // Configure target leaks (in bytes)
  14.             long targetLeak1 = 2L * 1024 * 1024 * 1024;  // 2GB for HeapMalloc1
  15.             long targetLeak2 = new Random().Next(500, 1000) * 1024L * 1024;  // 500MB-1GB for HeapMalloc2
  16.             long targetLeak3 = new Random().Next(500, 1000) * 1024L * 1024;  // 500MB-1GB for HeapMalloc3
  17.             // Chunk size (e.g., 100MB per iteration)
  18.             int chunkSize = 100 * 1024 * 1024;
  19.             // Thread 1: Leak 2GB in chunks
  20.             Thread thread1 = new Thread(() =>
  21.             {
  22.                 long leaked = 0;
  23.                 while (leaked < targetLeak1)
  24.                 {
  25.                     int allocate = (int)Math.Min(chunkSize, targetLeak1 - leaked);
  26.                     HeapMalloc1(allocate);
  27.                     leaked += allocate;
  28.                     Console.WriteLine($"HeapMalloc1: Leaked {leaked / (1024 * 1024)}MB / {targetLeak1 / (1024 * 1024)}MB");
  29.                     Thread.Sleep(100); // Delay between allocations
  30.                 }
  31.             });
  32.             // Thread 2: Leak 500MB-1GB in chunks
  33.             Thread thread2 = new Thread(() =>
  34.             {
  35.                 long leaked = 0;
  36.                 while (leaked < targetLeak2)
  37.                 {
  38.                     int allocate = (int)Math.Min(chunkSize, targetLeak2 - leaked);
  39.                     HeapMalloc2(allocate);
  40.                     leaked += allocate;
  41.                     Console.WriteLine($"HeapMalloc2: Leaked {leaked / (1024 * 1024)}MB / {targetLeak2 / (1024 * 1024)}MB");
  42.                     Thread.Sleep(100);
  43.                 }
  44.             });
  45.             // Thread 3: Leak 500MB-1GB in chunks
  46.             Thread thread3 = new Thread(() =>
  47.             {
  48.                 long leaked = 0;
  49.                 while (leaked < targetLeak3)
  50.                 {
  51.                     int allocate = (int)Math.Min(chunkSize, targetLeak3 - leaked);
  52.                     HeapMalloc3(allocate);
  53.                     leaked += allocate;
  54.                     Console.WriteLine($"HeapMalloc3: Leaked {leaked / (1024 * 1024)}MB / {targetLeak3 / (1024 * 1024)}MB");
  55.                     Thread.Sleep(100);
  56.                 }
  57.             });
  58.             // Start all threads
  59.             thread1.Start();
  60.             thread2.Start();
  61.             thread3.Start();
  62.             // Wait for completion
  63.             thread1.Join();
  64.             thread2.Join();
  65.             thread3.Join();
  66.             Console.WriteLine("All leaks completed!");
  67.             Console.ReadLine();
  68.         }
  69.     }
  70. }
复制代码
启动dottrace跟踪,跟踪完成之后,在 Filters 面板中有一个 Native Allocations 项,上面记录了当前程序已泄露 3.5G 内存,截图如下:
7.png

说实话有一点我想吐槽,dotTrace 为什么要将 Native Memory 和 NtHeap 做等价,Ntheap 只是 Native Memory 的子集,会让人觉得 Stack泄露,VirtualAlloc泄露都归到当前的 Native Allocations 中,这是一个很大的误解,所以更合适的名字叫 NtHeap Allocations。
接下来选中 Native Allocations 项下钻,可以清楚的看到各个线程泄露的百分比以及对应的函数,截图如下:
8.png

到这里我们终于知道原来 HeapMalloc1泄露了2G内存,HeapMalloc2泄露了800M内存,HeapMalloc3泄露了 640M 内存,真相大白。
三:总结

是不是觉得非常的棒,大家以后在分析托管或非托管内存的时候,在必要的场景下记得用 dottrace 哦。
9.jpg
作为JetBrains社区内容合作者,如有购买jetbrains的产品,可以用我的折扣码 HUANGXINCHENG,有25%的内部优惠哦。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册