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

推荐订阅源

Google DeepMind News
Google DeepMind News
博客园 - 聂微东
Vercel News
Vercel News
aimingoo的专栏
aimingoo的专栏
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
大猫的无限游戏
大猫的无限游戏
GbyAI
GbyAI
Martin Fowler
Martin Fowler
M
MIT News - Artificial intelligence
The GitHub Blog
The GitHub Blog
博客园_首页
博客园 - 叶小钗
腾讯CDC
G
Google Developers Blog
Blog — PlanetScale
Blog — PlanetScale
宝玉的分享
宝玉的分享
D
Docker

博客园 - 一线码农

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

一:背景

1. 讲故事

前几周分析了一个 40G+ 大内存的dump,这个程序平时最多不到30G,但不知道为啥最近会涨到40G,所以让我帮忙分析下怎么回事,像这种大内存dump,如果用传统的方式分析将会是一场灾难,这篇就来详细的说一说,从 windbg 的最佳分析实践来看,一个dump最好不要超过10G,否则就会遇到

  • dump跨机器分发慢。
  • dump命令处理反馈慢,一个sos命令可能需要数小时,这个是常人无法承受的。
  • 分析过程中容易引发机器内存不足告警,毕竟这种dump远大于 开发者的机器内存。

对于80%的场景都适合本条建议,但也有一些例外,比如有一些程序会使用大量缓存,所以内存常常维持在40G的高位,一旦此体量下的程序又出现了内存意外泄露,这种污水混在净水里,很难将其精准的摘出来。

二:超高内存分析方法

1. 一个简单的案例

由于电脑内存有限,我就以常规的 4G6G 来给大家做个演示,代码的意思非常简单,平时4G是因为有3个缓存实例 MemoryCache,后来不知道什么原因在 _staticCache 中灌入了2G数据,导致了非人意的场景发生,完整的参考代码如下:


public class MemorySpikeSimulator
{
    private static readonly MemoryCache _memoryCache1 = new MemoryCache("Cache1");
    private static readonly MemoryCache _memoryCache2 = new MemoryCache("Cache2");
    private static readonly MemoryCache _memoryCache3 = new MemoryCache("Cache3");

    private static readonly List<byte[]> _staticCache = new List<byte[]>();

    public static void Main()
    {
        Console.WriteLine("模拟内存增长测试...");
        Console.WriteLine("初始内存: " + FormatBytes(GC.GetTotalMemory(false)));

        // 模拟正常业务操作 - 三个MemoryCache共4GB
        SimulateNormalOperations();

        Console.WriteLine("正常操作后内存: " + FormatBytes(GC.GetTotalMemory(false)));

        Console.WriteLine("按Enter进行 意外内存 分配阶段... ");
        Console.ReadLine();

        // 模拟意外的大内存分配 - _staticCache增加2GB
        SimulateUnexpectedAllocation();

        Console.WriteLine("意外分配后内存: " + FormatBytes(GC.GetTotalMemory(false)));

        GC.Collect();
        GC.WaitForPendingFinalizers();

        Console.WriteLine("GC后内存: " + FormatBytes(GC.GetTotalMemory(true)));
        Console.WriteLine("按任意键退出...");
        Console.ReadKey();
    }

    private static void SimulateNormalOperations()
    {
        Console.WriteLine("开始正常内存分配 (三个MemoryCache共4GB)...");

        // 三个MemoryCache实例共同分配约4GB
        for (int i = 0; i < 350; i++) // 增加循环次数以达到4GB
        {
            var buffer1 = new byte[4 * 1024 * 1024]; // 4MB
            _memoryCache1.Add($"cache1_key_{i}", buffer1, DateTimeOffset.Now.AddHours(1));
            var buffer2 = new byte[4 * 1024 * 1024]; // 4MB
            _memoryCache2.Add($"cache2_key_{i}", buffer2, DateTimeOffset.Now.AddHours(1));
            var buffer3 = new byte[4 * 1024 * 1024]; // 4MB
            _memoryCache3.Add($"cache3_key_{i}", buffer3, DateTimeOffset.Now.AddHours(1));

            if (i % 50 == 0)
            {
                Console.WriteLine($"已分配: {(i + 1) * 12}MB");
            }

            Thread.Sleep(10); // 稍微减慢速度
        }
    }

    private static void SimulateUnexpectedAllocation()
    {
        Console.WriteLine("开始意外内存分配 (_staticCache增加2GB)...");

        // _staticCache意外增加约2GB
        for (int i = 0; i < 200; i++)
        {
            var unexpectedData = new byte[10 * 1024 * 1024]; // 10MB
            _staticCache.Add(unexpectedData);

            if (i % 20 == 0)
            {
                Console.WriteLine($"已分配: {(i + 1) * 10}MB");
            }

            Thread.Sleep(1);
        }
    }

    private static string FormatBytes(long bytes)
    {
        string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
        int counter = 0;
        decimal number = bytes;

        while (Math.Round(number / 1024) >= 1)
        {
            number /= 1024;
            counter++;
        }

        return $"{number:n2} {suffixes[counter]}";
    }
}

2. 分析方法简述

对于一个超大内存的dump,使用常规直接抓dump的方式不是最优方案,所以先需要用微软提供的 vmmap 观察进程的内存地址段布局,看下是托管内存,NTHeap 还是 VirtualAlloc 的泄露,不同的泄露有不同的应灾方案,截图如下:

从卦中可以清晰的看到,总计 6.6G 的内存,托管堆就吃了 6.3G,所以这个问题就被定性为 托管内存泄露。

问题被定性之后,接下来在生产环境上 正常内存时段异常内存时段 场景下各采1个dump,即 4G 和 6G 场景,这里稍微提醒下,采dump的方式相比dotmemory,perfview 附加进程方式的开销是最小的,dump采到之后,使用 perfview 的 Collect -> Take Heap Snapshot From Dump 或者将 dump 拖到 perfview 里,最终会构建出二个不到1M的 xxx.gcdump 文件,完整的截图如下:

文件有了之后接下来就是借助 perfview 的gcdump对比功能了,分别打开 xxx.dmp.gcdump 下的 Heap Stacks 子窗口,删除 GroupPats 框中的默认分组,接下来准备用 snapshot2 去对比 snapshot1,选择 Diff -> With Baseline xxx,截图如下:

在自动打开的新窗口中,可以很明显的看到增长的2G内存都是被 _staticCache 静态变量给吃掉了,截图如下:

到此真相大白,最后稍微提醒一下,如果发现是 ntheap 泄露,那就可以提前开启 ust 了。

三:总结

分析生产环境下的超大内存程序的故障,还是有一定的挑战的,大家也看到了这需要多工具的灵活运用,才能将不利影响降到最低。

图片名称