一:背景

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. php代码审计实战-开源项目Materialized CMS漏洞检测

    一.下载Materialized CMS 链接地址:https://sourceforge.net/projects/materialized-cms/files/latest/download 二. ...

  2. git 取消 git add 操作

    ... 按照套路我们在对项目做了一些新增或修改操作后,会很自然的执行 git add 操作, 但是马上又发现好像添加的内容有点不对: 文件名错了 多了个符号 少了点什么 马上发现bug 等等... 总 ...

  3. AI时代:本地运行大模型vllm

    https://docs.vllm.ai/en/latest/index.html 高吞吐量.高内存效率的 LLMs 推理和服务引擎(快速搭建本地大模型,且openAI API 兼容) vLLM is ...

  4. 一个Bug让人类科技倒退几十年?

    大家好,我是良许. 前几天在直播的时候,问了直播间的小伙伴有没人知道「千年虫」这种神奇的「生物」的,居然没有一人能够答得上来的. 所以,今天就跟大家科普一下这个人类历史上最大的 Bug . 1. 全世 ...

  5. File类使用详解

    File类是java io包下代表与平台无关的文件和目录,也就是说,在程序中操作文件和目录都可以通过File类来完成.但是File不能访问文件内容本身,访问文件内容需要使用输入/输出流. File类的 ...

  6. 移动开发webapp开发常用meta设置手机浏览器全屏模式,webappmeta

    1.WebApp全屏模式: <meta name="viewport" content="width=device-width,initial-scale=1.0, ...

  7. Spring基于xml的CRUD

    目录 基于xml的CRUD 代码实现 测试 基于xml的CRUD 源码 使用C3P0连接池 使用dbutils包中的QueryRunner类来对数据库进行操作 代码实现 pom.xml <?xm ...

  8. Java 线程的同步与死锁

    目录 1.线程的同步产生的原因 2.线程的同步处理操作 3.线程的死锁情况 排查死锁的方式: 请解释多个线程访问统一资源时需要考虑哪些情况?有可能带来哪些后果? 概念:Java同步和异步,阻塞和非阻塞 ...

  9. 【BUG】nuget restore遇到的两个报错“Failed to load msbuild Toolset”和“当前 .NET SDK 不支持将 .NET 6.0 设置为目标”

    出错环境: Visual Studio 2019 1. Failed to load msbuild Toolset 解决:https://github.com/NuGet/Home/issues/4 ...

  10. 记一次docker buildx build 推送到本地私有仓库出现 connection refused 的问题

    想在本地编译多个架构的基础镜像,这样后续有其他业务使用的时候,不必从头开始编译. 使用传统的 docker build -t ImageName:tag 方式,只能编译和主机相同架构的镜像. 而doc ...