惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
J
Java Code Geeks
P
Proofpoint News Feed
Recent Announcements
Recent Announcements
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园_首页
Hugging Face - Blog
Hugging Face - Blog
MongoDB | Blog
MongoDB | Blog
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
雷峰网
雷峰网
D
DataBreaches.Net
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Martin Fowler
Martin Fowler
有赞技术团队
有赞技术团队
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta

博客园 - 一线码农

记一次 .NET 某珠宝公司内部管理系统 内存暴涨分析 记一次 .NET 某智慧医保云服务Linux 非托管泄露分析 .NET 高级调试技术:超越基础 Dump 分析 .NET 生产环境调试实战指南 记一次 .NET 某光电成像后端解析系统 内存暴涨分析 记一次 .NET 某电力后台监控系统 内存暴涨分析 记一次 .NET 某注塑模具系统 CPU爆高分析 记一次 .NET 某集群管理软件 内存暴涨分析 记一次 .NET 某智慧工厂视觉程序 崩溃分析 记一次 .NET 某低代码开发框架 内存暴涨分析 记一次 .NET 某MES上位机拍照系统 内存暴涨分析 记一次 .NET 某RFID标签打印客户端 崩溃分析 对 .NET FileSystemWatcher引发内存碎片化的 反思 DotMemory系列:5. 如何实现自动化抓取和应用自托管 DotMemory系列:3. 堆碎片化引发的内存暴涨分析 DotMemory系列:2. 事件泄露引发的内存暴涨分析 DotMemory系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某理财管理客户端 OOM溢出分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 聊一聊 .NET 中的 CancellationTokenSource 记一次 .NET 某CRM物流行业管理系统 崩溃分析
DotMemory系列:4. 如何分析进程的转储文件
一线码农 · 2025-11-17 · via 博客园 - 一线码农

一:背景

1. 讲故事

前面几篇文章说的都是对进程采集 snapshot 文件,但这种方式的前提需要在目标机器上运行 DotMemory 相关组件,这在很多生产环境下很难做到,我知道很多医疗,金融生产环境,部署一个外来文件都需要层层审批,尤其像 dotmemory 这种商业软件,想上去门到没有。。。

目前主流的做法就是生成dump文件拿到线下分析,如果 dotmemory 不集成这块生态,那就是自绝于天地,接下来我们就来一起研究下。

二:分析转储文件

1. 测试代码

为了方便演示,我故意模拟一个内存分配的快进快出案例,即在一个方法内分配大量的临时对象,观察内存的上涨情况,参考代码如下:


internal class Program
{
    static void Main(string[] args)
    {
        ProcessMemoryAllocation();
        Console.ReadLine();
    }

    static void ProcessMemoryAllocation()
    {
        Console.WriteLine("开始分配内存...");

        // 分配 100MB 数组
        long bytes100MB = (long)100 * 1024 * 1024;
        byte[] array100MB = new byte[bytes100MB];
        array100MB[0] = 0x01;
        array100MB[array100MB.Length - 1] = 0xFF;
        Console.WriteLine($"✓ 100MB 数组分配成功: {bytes100MB:N0} 字节");

        // 分配 500MB 数组
        long bytes500MB = (long)500 * 1024 * 1024;
        byte[] array500MB = new byte[bytes500MB];
        array500MB[0] = 0x02;
        array500MB[array500MB.Length - 1] = 0xFE;
        Console.WriteLine($"✓ 500MB 数组分配成功: {bytes500MB:N0} 字节");

        // 分配 1GB 数组
        long bytes1GB = (long)1024 * 1024 * 1024;
        byte[] array1GB = new byte[bytes1GB];
        array1GB[0] = 0x03;
        array1GB[array1GB.Length - 1] = 0xFD;
        Console.WriteLine($"✓ 1GB 数组分配成功: {bytes1GB:N0} 字节");

        long totalBytes = bytes100MB + bytes500MB + bytes1GB;
        Console.WriteLine($"分配完成!");
        Console.WriteLine($"总计分配: {totalBytes:N0} 字节");
        Console.WriteLine($"约 {totalBytes / (1024.0 * 1024.0):F2} MB");
        Console.WriteLine($"约 {totalBytes / (1024.0 * 1024.0 * 1024.0):F2} GB");

    }
}

程序运行起来之后,可以看到当前程序吃了 1.7G 的内存,接下来用 process explorer 抓一个dump,截图如下:

2. 使用 dotmemory 分析

点击 Import Process Dump 按钮,找到要分析的文件,稍等之后就能看到快速检测台,这里要注意一下,dump文件越大,稍等的时间就越长,也可能等了几十分钟后结果爆了个内存不足,也是无语了,它的底层是通过一个单独的 MemoryDumpConverter.exe 程序来抽取托管堆数据的,截图如下:

由于程序吃了 1.7G,所以要观察下到底是谁吃掉了,观察 Overview 视图,截图如下:

从卦中可以看到 .NET 使用了 1.59G 内存,其中活对象是 105k ,看到这个数据很容易让人联想到上一篇说的 碎片化问题, 那到底是不是呢?

3. 是堆碎片化吗

进入 Inspections 视图,观察 Heap Fragmentation 区域,发现 LOH 上的 free=1.59G,这就让人很无语了,截图如下:

既然整条的 segment 都是 free,为什么gc不在上一阶段完整的回收它呢?带着忐忑不安的心情发现是上图中有一串数字 1.59GB occupied by 76 unreachable objects,啊,,, 原来是待回收的垃圾对象呀,但这个对象目前还不是 free 对象呀,为什么要把它当作 free 处理呢? 这种处理方式和传统的 windbg 处理模式完全不一样,需要适应一样,哈哈。

接下来点击这 76 个不可达对象,可以看到是 3 个超大的 byte[] 给吞掉了,截图如下:

dump 的单个分析我们差不多搞定了,接下来研究下双dump分析。

三. 双 dump 增量分析

1. 测试代码

为了能够捕获内存增量,修改了一下代码,在增量的第一和第三阶段各采一个dump文件,分别为 100M+ 和 1.7G+,代码和截图如下:


static void ProcessMemoryAllocation()
{
    Console.WriteLine("开始分配内存...");

    // 分配 100MB 数组
    long bytes100MB = (long)100 * 1024 * 1024;
    byte[] array100MB = new byte[bytes100MB];
    array100MB[0] = 0x01;
    array100MB[array100MB.Length - 1] = 0xFF;
    Console.WriteLine($"1. 100MB 数组分配成功: {bytes100MB:N0} 字节");

    Thread.Sleep(5000);

    // 分配 500MB 数组
    long bytes500MB = (long)500 * 1024 * 1024;
    byte[] array500MB = new byte[bytes500MB];
    array500MB[0] = 0x02;
    array500MB[array500MB.Length - 1] = 0xFE;
    Console.WriteLine($"2. 500MB 数组分配成功: {bytes500MB:N0} 字节");

    Thread.Sleep(5000);

    // 分配 1GB 数组
    long bytes1GB = (long)1024 * 1024 * 1024;
    byte[] array1GB = new byte[bytes1GB];
    array1GB[0] = 0x03;
    array1GB[array1GB.Length - 1] = 0xFD;
    Console.WriteLine($"3. 1GB 数组分配成功: {bytes1GB:N0} 字节");

    Thread.Sleep(5000);

    long totalBytes = bytes100MB + bytes500MB + bytes1GB;
    Console.WriteLine($"分配完成!");
    Console.WriteLine($"总计分配: {totalBytes:N0} 字节");
    Console.WriteLine($"约 {totalBytes / (1024.0 * 1024.0):F2} MB");
    Console.WriteLine($"约 {totalBytes / (1024.0 * 1024.0 * 1024.0):F2} GB");

    Console.ReadLine();
}

这两个dump都有了,如何比较增量呢? 做法比较简单,先将两个 dump 分别导入到 workspace 中,然后随意在一个 overview 中选择 Compare with snapshot from another workspace,即跨工作区比较,截图如下:

点击完成之后,就可以看到两个 snapshot 宏观对比,并且在左边区域中可以看到增量为 1.49G,截图如下:

鸟瞰图并没有解决问题,所以还要点击右上角的 Compare 观察堆上的对象详情,截图如下:

卦中有两个指标可以看懂。

  1. Objects delta: 快照1 (ObjectsA) 和 快照2 (ObjectB) 之间的数量增量。
  2. Bytes delta: 快照1 (BytesA) 和 快照2 (BytesB) 之间的占用增量。

对比之后终于知道,疑似有 4 个超大的 byte[] 吃掉了 1.49G 的内存。

不过很遗憾的是,在快照对比中看不到这 4个byte[] 的具体详情,what a pity 。。。

接下来怎么办呢? 只能单独进 1.59G 的snapshot,在 Types 选项中找到 byte[] ,然后双击进入即可,截图如下:

四:总结

dotmemory 整合了dump 转储体系,如虎添翼,还是那句话,它是技术支持工程师的好助手。

图片名称