DotTrace系列:7. 诊断 托管和非托管 内存暴涨
一:背景
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 Memory
和 NtHeap
做等价,Ntheap 只是 Native Memory
的子集,会让人觉得 Stack泄露,VirtualAlloc泄露都归到当前的 Native Allocations
中,这是一个很大的误解,所以更合适的名字叫 NtHeap Allocations
。
接下来选中 Native Allocations
项下钻,可以清楚的看到各个线程泄露的百分比以及对应的函数,截图如下:
到这里我们终于知道原来 HeapMalloc1泄露了2G内存,HeapMalloc2泄露了800M内存,HeapMalloc3泄露了 640M 内存,真相大白。
三:总结
是不是觉得非常的棒,大家以后在分析托管或非托管内存的时候,在必要的场景下记得用 dottrace 哦。
作为JetBrains社区内容合作者,如有购买jetbrains的产品,可以用我的折扣码 HUANGXINCHENG,有25%的内部优惠哦。
DotTrace系列:7. 诊断 托管和非托管 内存暴涨的更多相关文章
- 重学c#系列——c# 托管和非托管资源(三)
前言 c# 托管和非托管比较重要,因为这涉及到资源的释放. 现在只要在计算机上运行的,无论玩出什么花来,整个什么概念,逃不过输入数据修改数据输出数据(计算机本质),这里面有个数据的输入,那么我们的内存 ...
- 有关 Azure IaaS VM 磁盘以及托管和非托管高级磁盘的常见问题解答
本文将对有关 Azure 托管磁盘和 Azure 高级存储的一些常见问题进行解答. 托管磁盘 什么是 Azure 托管磁盘? 托管磁盘是一种通过处理存储帐户管理来简化 Azure IaaS VM 的磁 ...
- [.net 面向对象程序设计进阶] (8) 托管与非托管
本节导读:虽然在.NET编程过程中,绝大多数内存垃圾回收由CLR(公共语言运行时)自动回收,但也有很多需要我们编码回收.掌握托管与非托管的基本知识,可以有效避免某些情况下导致的程序异常. 1.什么是托 ...
- NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配
在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...
- C# 托管和非托管混合编程
在非托管模块中实现你比较重要的算法,然后通过 CLR 的平台互操作,来使托管代码调用它,这样程序仍然能够正常工作,但对非托管的本地代码进行反编译,就很困难. 最直接的实现托管与非托管编程的方法就是 ...
- C# using 三种使用方式 C#中托管与非托管 C#托管资源和非托管资源区别
1.using指令.using + 命名空间名字,这样可以在程序中直接用命令空间中的类型,而不必指定类型的详细命名空间,类似于Java的import,这个功能也是最常用的,几乎每个cs的程序都会用到. ...
- 利用C#Marshal类实现托管和非托管的相互转换
Marshal 类 命名空间:System.Runtime.InteropServices 提供了一个方法集,这些方法用于分配非托管内存.复制非托管内存块.将托管类型转换为非托管类型,此外还提供了在与 ...
- [转]C# 之DLL调用(托管与非托管)
每种编程语言调用DLL的方法都不尽相同,在此只对用C#调用DLL的方法进行介绍.首先,您需要了解什么是托管,什么是非托管.一般可以认为:非托管代码主要是基于win 32平台开发的DLL,activeX ...
- C#的托管与非托管大难点
托管代码与非托管代码 众所周知,我们正常编程所用的高级语言,是无法被计算机识别的.需要先将高级语言翻译为机器语言,才能被机器理解和运行.在标准C/C++中,编译过程是这样的:源代码首先经过预处理器,对 ...
- C#的三大难点之二:托管与非托管
相关文章: C#的三大难点之前传:什么时候应该使用C#?C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 ...
随机推荐
- 【WinForm】WinForm 生成单文件程序
WinForm 生成单文件程序 零.解决 安装 Costura.Fody 安装好这个库后生成的就是单文件了. .Net 3.5 NuGet控制台 NuGet\Install-Package Costu ...
- JBoltAI Function Call技术解析:如何实现AI模型与企业系统的无缝对话
JBoltAI Function Call技术解析: 如何实现AI模型与企业系统的无缝对话 在企业级AI应用开发中,如何让大模型能力与现有系统高效协同一直是技术难点.JBoltAI框架通过Functi ...
- 让IE6、IE7、IE8支持CSS3的圆角、阴影样式-最好的插件
想做个页面用到css3的圆角和阴影效果,但ie浏览器不支持,之前也听说有插件可以实现,周六在网上找到了一个方法,原文如下: 但凡是前端工程师,都知道IE6,IE7,IE8不支持.或者不完全支持CSS3 ...
- 在WampServer下增加PHP版本
WampServer更新比较慢,需要新版本的php时就需要我们自己去添加了. 步骤(这里默认你已经安装好了wampserver): 下载解压: 增加配置文件 重启wampserver 去php官网根据 ...
- Less中实现响应式设计的4种高效方案(手机、平板、电脑端)
下是4种纯Less实现的响应式方案,均封装成可复用方法. 方案1:基础设备混合封装 // 定义设备断点变量 @mobile-max: 767px; @tablet-min: 768px; @table ...
- Python 3.14 t-string 要来了,它与 f-string 有何不同?
Python 最近出了个大新闻:PEP-750 t-string 语法被正式采纳了! 这意味着 Python 将在今年 10 月发布的 3.14 版本中引入一种新的字符串前缀 t,称为模板字符串(Te ...
- 17.6K star!后端接口零代码的神器来了,腾讯开源的ORM库太强了!
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 " 实时零代码.全功能.强安全 ORM 库 后端接口和文档零代码,前端定制返回 J ...
- 如何在 Linux 上检查开放的端口并关闭不需要的端口
检查服务器开放端口并关闭不必要的端口是网络安全管理中的关键环节,开放端口如同服务器的"窗口",若其中存在未被利用或未受保护的端口,就如同为潜在的攻击者敞开了大门,他们可能会利用这些 ...
- Ubuntu20.04 搭建Kubernetes 1.28版本集群
环境依赖 以下操作,无特殊说明,所有节点都需要执行 安装 ssh 服务 安装 openssh-server sudo apt-get install openssh-server 修改配置文件 vim ...
- AI编译器及TVM整体架构
虽然之前也依据tvm官方文档写过一篇关于TVM架构的博客,但总感觉属于一种身在此山中的感觉(偏向于TVM实现的各个模块),并没有一览众山小的即视感(对框架的整体理解). 因此,今天再次请求出战... ...