一:背景

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. 自定义异常--java进阶day08

    1.自定义异常 2.自定义异常的格式 看你想要定义哪种异常,对应的继承哪种异常类 以我们之前写的代码举例,Exception类过于庞大,所有的异常子类都可以被它接收,这样就会导致无法精确捕获,所以我们 ...

  2. 【Python】介绍以及环境搭建

    Python简介 Python介绍 Python是时下最流流.最火爆的编程语言之一,具体原因如下: 简单.易学,适应人群广泛 免费.开源 应用领域广泛 备注:以下知名框架均是Python语言开发. G ...

  3. SQL Server 查看版本信息

    SQL Server 查看版本信息3种方法: 1) 使用命令行查看 [Win + R]键 -> 打开cmd 2) 使用SSMS查看 打开并连接SSMS后查看 3) 通过服务器属性查看 使用SSM ...

  4. .NET & JWT

    使用 JWT 库 JWT,a JWT(JSON Web Token) implementation for .NET 该库支持生成和解析JSON Web Token 你可以直接通过Nuget获取,也可 ...

  5. zk源码—6.Leader选举的实现原理

    大纲 1.zk是如何实现数据一致性的 (1)数据一致性分析 (2)实现数据一致性的广播模式 (3)实现数据一致性的恢复模式 2.zk是如何进行Leader选举的 (1)服务器启动时的Leader选举 ...

  6. Spring的基础结构和核心接口

    目录 1.BeanDefinition 2.BeanDefinitionReader 3.BeanDefinitionRegistry 4.BeanFactory 5.ApplicationConte ...

  7. Linux TCP网关的线程结构方案

    如果所示: 无论客户端还是服务端链接网关的socket都拆分为读EPoll.写EPoll分别独立. 有两个线程:线程A(左).线程B(右): 线程A负责服务端Socket的读和客户端socket的写, ...

  8. 参考案例之“对象调用方法时,如何在方法中使用对象,例如(root.display()的display方法中使用root)”

    一.对象调用方法时,如何在方法中使用对象,例如(root.display()的display方法中使用root) 1.测试方法 @Test public void suanfa24() { TreeN ...

  9. vue3 基础-补充 ref & provide-inject

    本篇主要对一些被以前内容(渲染, 传值) 等忽略的几个常用小技巧进行补充说明啦. v-once 即对某个dom节点生效, 其会限定只会渲染一次, 不论数据是如何的变化, 演示如下: <!DOCT ...

  10. Pandas中两个DataFrame的差集

    在pandas中,两个DataFrame的差集并没有直接的库内置方法,现在我们希望有一种方法,就像python中set内置的求差集一样,来找到两个DataFrame的差集. >>> ...