DotMemory系列:2. 事件泄露引发的内存暴涨分析
一:背景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 快照,可以看到如下的 检测台。
从检测台上可以看到如下三点信息:
[*]Largest Size 区域
前面的文章跟大家说过,这个区域是每个Type的浅层大小,可以看到 EventHandler 和 Room 联合吃了 940M 左右,和内存总量 1.15G 比较接近了,说明这两块是祸根,先重点备注一下。
[*]Largest Retained Size 区域
这个区域是以root根为出发点,并包含所有孩子节点的size,从图中可以看到 WifiManager 就属于其中的一个 root 根,有些人可能好奇它是什么 root 根? 可以单击 item 选择 Key Retention Path 选项,截图如下:
上面的 Regular local variable 表示局部变量,也就是说这个变量是栈引用根。
还有一点就是 EventHandler + Room 刚好接近 WifiManager 的总大小,说明前者应该都是它的孩子节点。
[*]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;
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<Example_9_9_2.WifiEventArgs>) (07FFAADB01A40h)
00007FFAAD7B172Cnop从卦中看上面的 rdx,qword ptr 就是我们的 Room 实例,然后通过 OnWiFiChanged 方法传递下去,即下面的 target 字段。
private void CtorClosed(object target, nint methodPtr)
{
if (target == null)
{
ThrowNullThisInDelegateToInstance();
}
_target = target;
_methodPtr = methodPtr;
}三:总结
是不是挺有意思的, DotMemory 这些界面真的是太有爱了。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! 用心讨论,共获提升! 懂技术并乐意极积无私分享的人越来越少。珍惜 前排留名,哈哈哈 分享、互助 让互联网精神温暖你我 懂技术并乐意极积无私分享的人越来越少。珍惜 很好很强大我过来先占个楼 待编辑 新版吗?好像是停更了吧。 收藏一下 不知道什么时候能用到 鼓励转贴优秀软件安全工具和文档! 新版吗?好像是停更了吧。 感谢发布原创作品,程序园因你更精彩 感谢分享,下载保存了,貌似很强大 这个好,看起来很实用 感谢分享 分享、互助 让互联网精神温暖你我 不错,里面软件多更新就更好了 收藏一下 不知道什么时候能用到 热心回复! 前排留名,哈哈哈
页:
[1]
2