大爷闪亮登场 發表於 2025-11-15 09:04:00

DotMemory系列:2. 事件泄露引发的内存暴涨分析

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>事件泄露导致的内存暴涨,说实话我以前是不敢相信的,因为我认为没人会写这样的代码,但现实往往都会打脸,还是太年轻了,今年年中的时候还真给遇到了,也算是无语啦,这一篇我们就来聊一聊如何通过 DotMemory 来一探究竟。</p>
<h2 id="二内存暴涨分析">二:内存暴涨分析</h2>
<h3 id="1-问题代码">1. 问题代码</h3>
<p>为了方便讲述,先来一段测试代码,代码非常简单,也就调用 1kw 次 SomeOperation 方法,调用完之后使用 <code> GC.Collect()</code> 强行回收,参考代码如下:</p>
<pre><code class="language-C#">
    internal class Program
    {
      static void Main(string[] args)
      {
            WiFiManager wifiManager = new WiFiManager();

            for (int i = 0; i &lt; 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&lt;WifiEventArgs&gt; 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 { }

</code></pre>
<p>接下来使用 DotMemory 的默认配置(采样模式)跟踪程序,会发现即使触发了 FullGC ,内存还维持1.15G左右,很明显存在内存泄露,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144938-533906440.png" alt="" loading="lazy"></p>
<p>接下来就是找原因了,为什么会这样?</p>
<h3 id="2-问题分析">2. 问题分析</h3>
<p>要想找原因,必须用 <code>Get Snapshot</code> 采一个快照下来,采集完成之后打开 <code>Snapshot #1</code> 快照,可以看到如下的 检测台。</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144960-1997337984.png" alt="" loading="lazy"></p>
<p>从检测台上可以看到如下三点信息:</p>
<ol>
<li>Largest Size 区域</li>
</ol>
<p>前面的文章跟大家说过,这个区域是每个Type的浅层大小,可以看到 <code>EventHandler&lt;WifiEventArgs&gt;</code> 和 <code>Room</code> 联合吃了 940M 左右,和内存总量 <code>1.15G</code> 比较接近了,说明这两块是祸根,先重点备注一下。</p>
<ol start="2">
<li>Largest Retained Size 区域</li>
</ol>
<p>这个区域是以root根为出发点,并包含所有孩子节点的size,从图中可以看到 WifiManager 就属于其中的一个 root 根,有些人可能好奇它是什么 root 根? 可以单击 item 选择 <code>Key Retention Path</code> 选项,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144960-1096076483.png" alt="" loading="lazy"></p>
<p>上面的 <code>Regular local variable</code> 表示局部变量,也就是说这个变量是栈引用根。</p>
<p>还有一点就是 <code>EventHandler&lt;WifiEventArgs&gt; + Room</code> 刚好接近 <code>WifiManager</code> 的总大小,说明前者应该都是它的孩子节点。</p>
<ol start="3">
<li>Event handlers leak</li>
</ol>
<p>从英文解释上就能知道,这个列表中的类实例是被订阅到别人的事件上,并且还没有 <code>解订阅</code>,那这样的对象有多少呢? 从列表中就可以看到有 <code>1000w</code> 的 Room,这个在数据上是一个异常信号。虽然 <code>Retained Size=228.88M</code>,但这个只算了浅层大小,深层大小不得而知。</p>
<p>有了上面三点信息之后,我们就从 Room 这个点出来,观察它的 root 链,单击 Room 类型之后再次选择 <code>Similar Retention</code> 选项,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144965-761997709.png" alt="" loading="lazy"></p>
<p>还有一点如果你想可视化观察,可以点击 <code>检测台</code> 上的 <code>Dominators</code> 选项卡观察 旭日图,这也是 DotMemory 快速可视化的一个亮点,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144953-1308610539.png" alt="" loading="lazy"></p>
<p>如果想要观察 <code>WifiManager</code> 类实例的内容也比较简单,这个也是 DotMemory 非常好的一个亮点,比如下图的 _invocationList[],这也是 多播调用 的底层核心,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251115090144945-1710130198.png" alt="" loading="lazy"></p>
<p>到这里就已经豁然开朗了,接下来就是去看 Room 是怎么挂接到 <code>WiFiManager.WiFiSignalChanged</code> 上,翻看源码很快就找到了问题,参考如下:</p>
<pre><code class="language-C#">
      public Room(WiFiManager wiFiManager)
      {
            wiFiManager.WiFiSignalChanged += OnWiFiChanged;
      }

</code></pre>
<p>可能有些人比较懵逼,我明明是把 OnWiFiChanged 方法注进去的,为什么当前的 this (room) 对象也进去了呢?</p>
<h3 id="3-为什么会注册-this">3. 为什么会注册 this</h3>
<p>要想找到这个答案,直接观察汇编即可,参考如下:</p>
<pre><code class="language-C#">
         // wiFiManager.WiFiSignalChanged += OnWiFiChanged;
00007FFAAD7B16F2mov         rcx,7FFAADAE8BF0h
00007FFAAD7B16FCcall      CORINFO_HELP_NEWSFAST (07FFB0D30FA50h)
00007FFAAD7B1701mov         qword ptr ,rax
00007FFAAD7B1705mov         rcx,qword ptr
00007FFAAD7B1709mov         rdx,qword ptr
00007FFAAD7B170Dmov         r8,offset Example_9_9_2.Room.OnWiFiChanged(System.Object, Example_9_9_2.WifiEventArgs) (07FFAADB022B0h)
00007FFAAD7B1717call      qword ptr
00007FFAAD7B171Dmov         rcx,qword ptr
00007FFAAD7B1721mov         rdx,qword ptr
00007FFAAD7B1725cmp         dword ptr ,ecx
00007FFAAD7B1727call      Example_9_9_2.WiFiManager.add_WiFiSignalChanged(System.EventHandler`1&lt;Example_9_9_2.WifiEventArgs&gt;) (07FFAADB01A40h)
00007FFAAD7B172Cnop

</code></pre>
<p>从卦中看上面的 <code>rdx,qword ptr </code> 就是我们的 Room 实例,然后通过 <code>OnWiFiChanged</code> 方法传递下去,即下面的 <code>target</code> 字段。</p>
<pre><code class="language-C#">
private void CtorClosed(object target, nint methodPtr)
{
        if (target == null)
        {
                ThrowNullThisInDelegateToInstance();
        }
        _target = target;
        _methodPtr = methodPtr;
}

</code></pre>
<h2 id="三总结">三:总结</h2>
<p>是不是挺有意思的, DotMemory 这些界面真的是太有爱了。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/19224329
頁: [1]
查看完整版本: DotMemory系列:2. 事件泄露引发的内存暴涨分析