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

推荐订阅源

G
Google Developers Blog
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
H
Help Net Security
B
Blog RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
S
SegmentFault 最新的问题
The Cloudflare Blog
I
InfoQ
美团技术团队
博客园 - 三生石上(FineUI控件)
MyScale Blog
MyScale Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
L
LangChain Blog
A
About on SuperTechFans
T
The Blog of Author Tim Ferriss
Y
Y Combinator Blog

博客园 - 一线码农

记一次 .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系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某理财管理客户端 OOM溢出分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 聊一聊 .NET 中的 CancellationTokenSource 记一次 .NET 某CRM物流行业管理系统 崩溃分析
DotMemory系列:2. 事件泄露引发的内存暴涨分析
一线码农 · 2025-11-15 · via 博客园 - 一线码农

一:背景

1. 讲故事

事件泄露导致的内存暴涨,说实话我以前是不敢相信的,因为我认为没人会写这样的代码,但现实往往都会打脸,还是太年轻了,今年年中的时候还真给遇到了,也算是无语啦,这一篇我们就来聊一聊如何通过 DotMemory 来一探究竟。

二:内存暴涨分析

1. 问题代码

为了方便讲述,先来一段测试代码,代码非常简单,也就调用 1kw 次 SomeOperation 方法,调用完之后使用 GC.Collect() 强行回收,参考代码如下:


    internal class Program
    {
        static void Main(string[] args)
        {
            WiFiManager wifiManager = new WiFiManager();

            for (int i = 0; i < 10000000; i++)
            {
                SomeOperation(wifiManager);
            }

            GC.Collect();
            Console.WriteLine("全部执行完成,GC也触发完毕!!!");
            Console.ReadKey();
        }

        static void SomeOperation(WiFiManager wifiManager)
        {
            var room = new Room(wifiManager);

            var wifiStatus = room.GetWifiStatus();
        }
    }

    public class WiFiManager
    {
        public event EventHandler<WifiEventArgs> WiFiSignalChanged;
    }

    public class Room
    {
        public Room(WiFiManager wiFiManager)
        {
            wiFiManager.WiFiSignalChanged += OnWiFiChanged;
        }

        private void OnWiFiChanged(object sender, WifiEventArgs e)
        {
        }

        public string GetWifiStatus()
        {
            return "wifi 状态良好...";
        }
    }

    public class WifiEventArgs : EventArgs { }

接下来使用 DotMemory 的默认配置(采样模式)跟踪程序,会发现即使触发了 FullGC ,内存还维持1.15G左右,很明显存在内存泄露,截图如下:

接下来就是找原因了,为什么会这样?

2. 问题分析

要想找原因,必须用 Get Snapshot 采一个快照下来,采集完成之后打开 Snapshot #1 快照,可以看到如下的 检测台。

从检测台上可以看到如下三点信息:

  1. Largest Size 区域

前面的文章跟大家说过,这个区域是每个Type的浅层大小,可以看到 EventHandler<WifiEventArgs>Room 联合吃了 940M 左右,和内存总量 1.15G 比较接近了,说明这两块是祸根,先重点备注一下。

  1. Largest Retained Size 区域

这个区域是以root根为出发点,并包含所有孩子节点的size,从图中可以看到 WifiManager 就属于其中的一个 root 根,有些人可能好奇它是什么 root 根? 可以单击 item 选择 Key Retention Path 选项,截图如下:

上面的 Regular local variable 表示局部变量,也就是说这个变量是栈引用根。

还有一点就是 EventHandler<WifiEventArgs> + Room 刚好接近 WifiManager 的总大小,说明前者应该都是它的孩子节点。

  1. Event handlers leak

从英文解释上就能知道,这个列表中的类实例是被订阅到别人的事件上,并且还没有 解订阅,那这样的对象有多少呢? 从列表中就可以看到有 1000w 的 Room,这个在数据上是一个异常信号。虽然 Retained Size=228.88M,但这个只算了浅层大小,深层大小不得而知。

有了上面三点信息之后,我们就从 Room 这个点出来,观察它的 root 链,单击 Room 类型之后再次选择 Similar Retention 选项,截图如下:

还有一点如果你想可视化观察,可以点击 检测台 上的 Dominators 选项卡观察 旭日图,这也是 DotMemory 快速可视化的一个亮点,截图如下:

如果想要观察 WifiManager 类实例的内容也比较简单,这个也是 DotMemory 非常好的一个亮点,比如下图的 _invocationList[],这也是 多播调用 的底层核心,截图如下:

到这里就已经豁然开朗了,接下来就是去看 Room 是怎么挂接到 WiFiManager.WiFiSignalChanged 上,翻看源码很快就找到了问题,参考如下:


        public Room(WiFiManager wiFiManager)
        {
            wiFiManager.WiFiSignalChanged += OnWiFiChanged;
        }

可能有些人比较懵逼,我明明是把 OnWiFiChanged 方法注进去的,为什么当前的 this (room) 对象也进去了呢?

3. 为什么会注册 this

要想找到这个答案,直接观察汇编即可,参考如下:


           // wiFiManager.WiFiSignalChanged += OnWiFiChanged;
00007FFAAD7B16F2  mov         rcx,7FFAADAE8BF0h  
00007FFAAD7B16FC  call        CORINFO_HELP_NEWSFAST (07FFB0D30FA50h)  
00007FFAAD7B1701  mov         qword ptr [rbp+28h],rax  
00007FFAAD7B1705  mov         rcx,qword ptr [rbp+28h]  
00007FFAAD7B1709  mov         rdx,qword ptr [rbp+50h]  
00007FFAAD7B170D  mov         r8,offset Example_9_9_2.Room.OnWiFiChanged(System.Object, Example_9_9_2.WifiEventArgs) (07FFAADB022B0h)  
00007FFAAD7B1717  call        qword ptr [Pointer to stub for: System.MulticastDelegate.CtorClosed(System.Object, IntPtr) (07FFAAD794210h)]  
00007FFAAD7B171D  mov         rcx,qword ptr [rbp+58h]  
00007FFAAD7B1721  mov         rdx,qword ptr [rbp+28h]  
00007FFAAD7B1725  cmp         dword ptr [rcx],ecx  
00007FFAAD7B1727  call        Example_9_9_2.WiFiManager.add_WiFiSignalChanged(System.EventHandler`1<Example_9_9_2.WifiEventArgs>) (07FFAADB01A40h)  
00007FFAAD7B172C  nop  

从卦中看上面的 rdx,qword ptr [rbp+50h] 就是我们的 Room 实例,然后通过 OnWiFiChanged 方法传递下去,即下面的 target 字段。


private void CtorClosed(object target, nint methodPtr)
{
	if (target == null)
	{
		ThrowNullThisInDelegateToInstance();
	}
	_target = target;
	_methodPtr = methodPtr;
}

三:总结

是不是挺有意思的, DotMemory 这些界面真的是太有爱了。

图片名称