一:背景

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+的内存,这明显是超出预期的,现在很惶恐,截图如下:

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

由于dump是非常保密的,不适合分发给第三方,在生产上搭建windbg工作台也不是很方便,有没有轻量级的工具直接分析呢?哈哈,这时候就可以考虑下 dotrrace,它可以帮你找出托管内存分配都是从哪个方法出来的。

使用 dotrace 初始化跟踪或者附加进程一段时间后,采集到的跟踪文件如下:

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

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

  • 托管内存主要被 LOH 大对象堆给吃了
  • 托管堆上最多的对象是 System.Byte[]

看到这里心里就踏实多了,接下来选中 System.Byte[],看下这些分配都藏在哪些方法里,接下来选择 Hotspots 中的 Plain List 选项,截图如下:

从卦中可以看到内存主要被 LoadCustomerAttachments 方法给吃掉了,接下来点击 Show Code 观察该方法源码,代码参考如下:


static void LoadCustomerAttachments()
{
Console.WriteLine($"[客户附件] 开始加载 (线程ID: {Thread.CurrentThread.ManagedThreadId})"); try
{
var attachments = new Dictionary<int, byte[]>();
for (int i = 0; i < 30; i++)
{
attachments[i] = new byte[100 * 1024 * 1024]; for (int j = 0; j < attachments[i].Length; j += 1024)
{
attachments[i][j] = (byte)(i + j);
} // 每5个附件输出一次进度
if (i % 5 == 0)
{
Console.WriteLine($"[客户附件] 已加载 {i} 个附件 ({(i + 1) * 100}MB)");
}
} Console.WriteLine($"[客户附件] 加载完成,共{attachments.Count}个附件");
}
catch (OutOfMemoryException ex)
{
Console.WriteLine($"[客户附件] 内存不足错误: {ex.Message}");
}
}

到这里基本就真相大白,是不是有点像 ust 效果。

2. 非托管内存暴涨

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

为了方便演示,我用 C# 调用 C++ 来实现一个NTHEAP 的非托管内存泄露,然后借助 dottrace 快速分析,首先定义几个C 导出函数,代码如下:


extern "C"
{
_declspec(dllexport) void HeapMalloc1(int bytes);
_declspec(dllexport) void HeapMalloc2(int bytes);
_declspec(dllexport) void HeapMalloc3(int bytes);
} #include "iostream"
#include <Windows.h> using namespace std; void HeapMalloc1(int bytes)
{
int* ptr = (int*)malloc(bytes);
printf("bytes=%d ,分配完毕\n", bytes);
} void HeapMalloc2(int bytes)
{
int* ptr = (int*)malloc(bytes);
printf("bytes=%d ,分配完毕\n", bytes); } void HeapMalloc3(int bytes)
{
int* ptr = (int*)malloc(bytes);
printf("bytes=%d ,分配完毕\n", bytes);
}

接下来通过C#不断的调用这几个函数,其中 HeapMalloc1 方法会泄露 2G 的内存,参考代码如下:


namespace MemoryLeakSimulator
{
internal class Program
{
[DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
extern static void HeapMalloc1(int bytes); [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
extern static void HeapMalloc2(int bytes); [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
extern static void HeapMalloc3(int bytes); static void Main(string[] args)
{
// Configure target leaks (in bytes)
long targetLeak1 = 2L * 1024 * 1024 * 1024; // 2GB for HeapMalloc1
long targetLeak2 = new Random().Next(500, 1000) * 1024L * 1024; // 500MB-1GB for HeapMalloc2
long targetLeak3 = new Random().Next(500, 1000) * 1024L * 1024; // 500MB-1GB for HeapMalloc3 // Chunk size (e.g., 100MB per iteration)
int chunkSize = 100 * 1024 * 1024; // Thread 1: Leak 2GB in chunks
Thread thread1 = new Thread(() =>
{
long leaked = 0;
while (leaked < targetLeak1)
{
int allocate = (int)Math.Min(chunkSize, targetLeak1 - leaked);
HeapMalloc1(allocate);
leaked += allocate;
Console.WriteLine($"HeapMalloc1: Leaked {leaked / (1024 * 1024)}MB / {targetLeak1 / (1024 * 1024)}MB");
Thread.Sleep(100); // Delay between allocations
}
}); // Thread 2: Leak 500MB-1GB in chunks
Thread thread2 = new Thread(() =>
{
long leaked = 0;
while (leaked < targetLeak2)
{
int allocate = (int)Math.Min(chunkSize, targetLeak2 - leaked);
HeapMalloc2(allocate);
leaked += allocate;
Console.WriteLine($"HeapMalloc2: Leaked {leaked / (1024 * 1024)}MB / {targetLeak2 / (1024 * 1024)}MB");
Thread.Sleep(100);
}
}); // Thread 3: Leak 500MB-1GB in chunks
Thread thread3 = new Thread(() =>
{
long leaked = 0;
while (leaked < targetLeak3)
{
int allocate = (int)Math.Min(chunkSize, targetLeak3 - leaked);
HeapMalloc3(allocate);
leaked += allocate;
Console.WriteLine($"HeapMalloc3: Leaked {leaked / (1024 * 1024)}MB / {targetLeak3 / (1024 * 1024)}MB");
Thread.Sleep(100);
}
}); // Start all threads
thread1.Start();
thread2.Start();
thread3.Start(); // Wait for completion
thread1.Join();
thread2.Join();
thread3.Join(); Console.WriteLine("All leaks completed!");
Console.ReadLine();
}
}
}

启动dottrace跟踪,跟踪完成之后,在 Filters 面板中有一个 Native Allocations 项,上面记录了当前程序已泄露 3.5G 内存,截图如下:

说实话有一点我想吐槽,dotTrace 为什么要将 Native MemoryNtHeap 做等价,Ntheap 只是 Native Memory 的子集,会让人觉得 Stack泄露,VirtualAlloc泄露都归到当前的 Native Allocations 中,这是一个很大的误解,所以更合适的名字叫 NtHeap Allocations

接下来选中 Native Allocations 项下钻,可以清楚的看到各个线程泄露的百分比以及对应的函数,截图如下:

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

三:总结

是不是觉得非常的棒,大家以后在分析托管或非托管内存的时候,在必要的场景下记得用 dottrace 哦。

作为JetBrains社区内容合作者,如有购买jetbrains的产品,可以用我的折扣码 HUANGXINCHENG,有25%的内部优惠哦。

DotTrace系列:7. 诊断 托管和非托管 内存暴涨的更多相关文章

  1. 重学c#系列——c# 托管和非托管资源(三)

    前言 c# 托管和非托管比较重要,因为这涉及到资源的释放. 现在只要在计算机上运行的,无论玩出什么花来,整个什么概念,逃不过输入数据修改数据输出数据(计算机本质),这里面有个数据的输入,那么我们的内存 ...

  2. 有关 Azure IaaS VM 磁盘以及托管和非托管高级磁盘的常见问题解答

    本文将对有关 Azure 托管磁盘和 Azure 高级存储的一些常见问题进行解答. 托管磁盘 什么是 Azure 托管磁盘? 托管磁盘是一种通过处理存储帐户管理来简化 Azure IaaS VM 的磁 ...

  3. [.net 面向对象程序设计进阶] (8) 托管与非托管

    本节导读:虽然在.NET编程过程中,绝大多数内存垃圾回收由CLR(公共语言运行时)自动回收,但也有很多需要我们编码回收.掌握托管与非托管的基本知识,可以有效避免某些情况下导致的程序异常. 1.什么是托 ...

  4. NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配

    在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...

  5. C# 托管和非托管混合编程

    在非托管模块中实现你比较重要的算法,然后通过 CLR 的平台互操作,来使托管代码调用它,这样程序仍然能够正常工作,但对非托管的本地代码进行反编译,就很困难.   最直接的实现托管与非托管编程的方法就是 ...

  6. C# using 三种使用方式 C#中托管与非托管 C#托管资源和非托管资源区别

    1.using指令.using + 命名空间名字,这样可以在程序中直接用命令空间中的类型,而不必指定类型的详细命名空间,类似于Java的import,这个功能也是最常用的,几乎每个cs的程序都会用到. ...

  7. 利用C#Marshal类实现托管和非托管的相互转换

    Marshal 类 命名空间:System.Runtime.InteropServices 提供了一个方法集,这些方法用于分配非托管内存.复制非托管内存块.将托管类型转换为非托管类型,此外还提供了在与 ...

  8. [转]C# 之DLL调用(托管与非托管)

    每种编程语言调用DLL的方法都不尽相同,在此只对用C#调用DLL的方法进行介绍.首先,您需要了解什么是托管,什么是非托管.一般可以认为:非托管代码主要是基于win 32平台开发的DLL,activeX ...

  9. C#的托管与非托管大难点

    托管代码与非托管代码 众所周知,我们正常编程所用的高级语言,是无法被计算机识别的.需要先将高级语言翻译为机器语言,才能被机器理解和运行.在标准C/C++中,编译过程是这样的:源代码首先经过预处理器,对 ...

  10. C#的三大难点之二:托管与非托管

    相关文章: C#的三大难点之前传:什么时候应该使用C#?​C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 ...

随机推荐

  1. Hack The Box-Chemistry靶机渗透

    通过信息收集访问5000端口,cif历史cve漏洞反弹shell,获取数据库,利用低权限用户登录,监听端口,开放8080端口,aihttp服务漏洞文件包含,获取root密码hash值,ssh指定登录 ...

  2. 【Docker】DockerFile解析

    DockerFile解析 中文官网 英文官网 是什么 Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本. 构建三步骤 注意:Dockerfile可以构建出镜像, ...

  3. 流式计算(五)-Flink核心概念

    一手资料,完全来自官网,直接参考英文过来的,并加了一些自己的理解,希望能让看官君了解点什么,足矣. 环境:Flink1.9.1 难度:新手--战士--老兵--大师 目标: 理解Flink的计算模型 认 ...

  4. Win10锁屏与关机相关设置-注册表

    禁用锁屏 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData ...

  5. 重生之我是操作系统(五)----CPU调度

    简介 当CPU有大量任务要处理,但由于资源有限,无法同时处理.所有就需要某种规则来决定任务处理的顺序,这就是调度. 调度层次 根据调度频率与层次,共分为三种 高级调度 也称为作业调度(Long-Tre ...

  6. Web前端入门第 31 问:CSS background 元素背景图用法全解

    background 可设置背景色.渐变.背景图等,本文主要讲解背景图片的用法. 背景顾名思义就是背后的景色,始终居于元素背后,元素永远站在背景的身前. 本文示例中所使用的图片: background ...

  7. 2025年4月TIOBE指数

    4 月头条:编程语言 Kotlin.Ruby 和 Swift 直到最近在 TIOBE 指数排名中都一直稳居前 20 的稳定位置.但如今它们似乎失去了发展动力,且很可能会逐渐过时.Kotlin 和 Sw ...

  8. [亲测]ThinkPHP中where方法中变量不解析的解决方法

    2018年5月4日 01:15  血的教训,今天做一个项目,需要批量更新数据,所以where中必须用变量.发现where里的变量不解析并且会直接报错,然后通过搜索发现可以在双引号中的左右加号中包裹变量 ...

  9. STM32F407数据手册中文版,STM32F429数据参考手册中文版

    发布一个适用STM32F405XX.STM32F407XX.STM32F415XX.STM32F417XX.STM32F427XX.STM32F437XX的中文数据手册,具体内容见下图: 首页 目录 ...

  10. hadoop问题解决(七)日志/重启/开机自启动

    6.1hadoop日志 Master节点 Slave节点 6.2 hadoop排错 (待补充) 6.3 spark 6.4 zookeeper 6.5 hive 6.6 kafka 7重启命令 7.1 ...