Release编译模式下,事件是否会引起内存泄漏问题初步研究
题记:不常发生的事件内存泄漏现象
想必有些朋友也常常使用事件,但是很少解除事件挂钩,程序也没有听说过内存泄漏之类的问题。幸运的是,在某些情况下,的确不会出问题,很多年前做的项目就跑得好好的,包括我也是,虽然如此,但也不能一直心存侥幸,总得搞清楚这类内存泄漏的神秘事件是怎么发生的吧,我们今天可以做一个实验来再次验证下。
可以,为了验证这个问题,我一度怀疑自己代码写错了,甚至照着书上(网上)例子写也无法重现事件引起内存泄漏的问题,难道教科书说错了么?
首先来看看我的代码,先准备2个类,一个发起事件,一个处理事件:
class A
{
public event EventHandler ToDoSomething ;
public A()
{
} public void RaiseEvent()
{
ToDoSomething(this, new EventArgs());
} public void DelEvent()
{
ToDoSomething = null;
} public void Print(string msg)
{
Console.WriteLine("A:{0}", msg); }
}
class B
{
byte[] data = null; public B(int size)
{
data = new byte[size];
for (int i = ; i < size ; i++)
data[i] = ;
} public void PrintA(object sender, EventArgs e)
{
((A)sender).Print("sender:"+ sender.GetType ());
}
}
然后,在主程序里面写下面的方法:
static void TestInitEvent(A a)
{
var b = new B( * * );
a.ToDoSomething += b.PrintA;
}
这里将初始化一个 100M的B的实例对象b,然后让对象a的事件ToDoSomething 挂钩在b的方法PrintA 上。平常情况下,b是方法内部的局部变量,在方法外就是不可访问的,但由于b对象的方法挂钩在了方法参数 a 对象的事件上,所以在这里对象 b的生命周期并没有结束,这可以稍后由对象 a发起事件,b的 PrintA 方法被调用得到证实。
PS:有朋友问为何不在这里写取消挂钩的代码,我这里是研究使用的,实际项目代码一般不会这么写。
为了监测当前测试耗费了多少内存,准备一个方法 getWorkingSet,代码如下:
static void getWorkingSet()
{
using (var process = Process.GetCurrentProcess())
{
Console.WriteLine("---------当前进程名称:{0}-----------",process.ProcessName);
using (var p1 = new PerformanceCounter("Process", "Working Set - Private", process.ProcessName))
using (var p2 = new PerformanceCounter("Process", "Working Set", process.ProcessName))
{
Console.WriteLine(process.Id);
//注意除以CPU数量
Console.WriteLine("{0}{1:N} KB", "工作集(进程类)", process.WorkingSet64 / );
Console.WriteLine("{0}{1:N} KB", "工作集 ", process.WorkingSet64 / );
// process.PrivateMemorySize64 私有工作集 不是很准确,大概多9M
Console.WriteLine("{0}{1:N} KB", "私有工作集 ", p1.NextValue() / ); //p1.NextValue()
//Logger("{0};内存(专用工作集){1:N};PID:{2};程序名:{3}",
// DateTime.Now, p1.NextValue() / 1024, process.Id.ToString(), process.ProcessName); }
}
Console.WriteLine("--------------------------------------------------------");
Console.WriteLine(); }
下面,开始在主程序里面开始写如下测试代码:
getWorkingSet();
A a = new A();
TestInitEvent(a);
Console.WriteLine("1,按下任意键开始垃圾回收");
Console.ReadKey();
GC.Collect();
getWorkingSet();
看屏幕输出:
---------当前进程名称:ConsoleApplication1.vshost-----------
4848
工作集(进程类)25,260.00 KB
工作集 25,260.00 KB
私有工作集 8,612.00 KB
-------------------------------------------------------- 1,按下任意键开始垃圾回收
---------当前进程名称:ConsoleApplication1.vshost-----------
4848
工作集(进程类)135,236.00 KB
工作集 135,236.00 KB
私有工作集 111,256.00 KB
程序开始运行后,正好多了100M内存占用。当前程序处于IDE的调试状态下,然后,我们直接运行测试程序,不调试(Release),再次看下结果:
---------当前进程名称:ConsoleApplication1-----------
7056
工作集(进程类)10,344.00 KB
工作集 10,344.00 KB
私有工作集 7,036.00 KB
-------------------------------------------------------- 1,按下任意键开始垃圾回收
---------当前进程名称:ConsoleApplication1-----------
7056
工作集(进程类)121,460.00 KB
工作集 121,460.00 KB
私有工作集 109,668.00 KB
--------------------------------------------------------
可以看到在Release 编译模式下,内存还是没法回收。
分析下上面这段测试程序,我们只是在一个单独的方法内挂钩了一个事件,并且事件还没有执行,紧接着开始垃圾回收,但结果显示没有回收成功。这个符合我们教科书上说的情况:对象的事件挂钩之后,如果不解除挂钩,可能造成内存泄漏。
同时,上面的结果也说明了被挂钩的对象 b 没有被回收,这可以发起事件来测试下,看b对象是否还能够继续处理对象a 发起的事件,继续上面主程序代码:
Console.WriteLine("2,按下任意键,主对象发起事件");
Console.ReadKey();
a.RaiseEvent();//此处内存不能正常回收
getWorkingSet();
结果:
,按下任意键,主对象发起事件
A:sender:ConsoleApplication1.A
---------当前进程名称:ConsoleApplication1----------- 工作集(进程类),576.00 KB
工作集 ,576.00 KB
私有工作集 ,672.00 KB
--------------------------------------------------------
这说明,虽然对象 b 脱离了方法 TestInitEvent 的范围,但它依然存活,打印了一句话:A:sender:ConsoleApplication1.A
是不是GC多回收几次才能够成功呢?
我们继续在主程序上调用GC试试看:
Console.WriteLine("3,按下任意键开始垃圾回收,之后再次发起事件");
Console.ReadKey();
GC.Collect();
a.RaiseEvent();//此处内存不能正常回收
getWorkingSet();
结果:
3,按下任意键开始垃圾回收,之后再次发起事件
A:sender:ConsoleApplication1.A
---------当前进程名称:ConsoleApplication1-----------
7056
工作集(进程类)14,424.00 KB
工作集 14,424.00 KB
私有工作集 2,972.00 KB
--------------------------------------------------------
果然,内存被回收了!
但请注意,我们在GC执行成功后,仍然调用了发起事件的方法 a.RaiseEvent();并且得到了成功执行,这说明,对象b 仍然存活,事件挂钩仍然有效,不过它内部大量无用的内存被回收了。
注意:上面这段代码的结果是我再写博客过程中,一边写一遍测试偶然发现的情况,如果是连续执行的,情况并不是这样,上面这端代码不能回收成功内存。
这说明,GC内存回收的时机,的确是不确定的。
继续,我们注销事件,解除事件挂钩,再看结果:
Console.WriteLine("4,按下任意键开始注销事件,之后再次垃圾回收");
Console.ReadKey();
a.DelEvent();
GC.Collect();
Console.WriteLine("5,垃圾回收完成");
getWorkingSet();
结果:
4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1-----------
7056
工作集(进程类)15,252.00 KB
工作集 15,252.00 KB
私有工作集 3,196.00 KB
--------------------------------------------------------
内存没有明显变化,说明之前的内存的确成功回收了。
为了印证前面的猜测,我们让程序重新运行并且连续执行(Release模式),来看看执行结果:
---------当前进程名称:ConsoleApplication1-----------
4280
工作集(进程类)10,364.00 KB
工作集 10,364.00 KB
私有工作集 7,040.00 KB
-------------------------------------------------------- 1,按下任意键开始垃圾回收
---------当前进程名称:ConsoleApplication1-----------
4280
工作集(进程类)121,456.00 KB
工作集 121,456.00 KB
私有工作集 109,668.00 KB
-------------------------------------------------------- 2,按下任意键,主对象发起事件
A:sender:ConsoleApplication1.A
---------当前进程名称:ConsoleApplication1-----------
4280
工作集(进程类)121,572.00 KB
工作集 121,572.00 KB
私有工作集 109,672.00 KB
-------------------------------------------------------- 3,按下任意键开始垃圾回收,之后再次发起事件
A:sender:ConsoleApplication1.A
---------当前进程名称:ConsoleApplication1-----------
4280
工作集(进程类)121,628.00 KB
工作集 121,628.00 KB
私有工作集 109,672.00 KB
-------------------------------------------------------- 4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1-----------
4280
工作集(进程类)19,228.00 KB
工作集 19,228.00 KB
私有工作集 7,272.00 KB
--------------------------------------------------------
这次的确印证了前面的说明,GC真正回收内存的时机是不确定的。
编译器的优化
精简下之前的测试代码,仅初始化事件对象然后就GC回收,看看结果:
getWorkingSet();
A a = new A();
TestInitEvent(a);
getWorkingSet(); Console.WriteLine("4,按下任意键开始注销事件,之后再次垃圾回收");
Console.ReadKey();
a.DelEvent();
GC.Collect();
Console.WriteLine("5,垃圾回收完成");
getWorkingSet();
Console.ReadKey();
结果:
---------当前进程名称:ConsoleApplication1-----------
6576
工作集(进程类)10,344.00 KB
工作集 10,344.00 KB
私有工作集 7,240.00 KB
-------------------------------------------------------- ---------当前进程名称:ConsoleApplication1-----------
6576
工作集(进程类)121,500.00 KB
工作集 121,500.00 KB
私有工作集 110,292.00 KB
-------------------------------------------------------- 4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1-----------
6576
工作集(进程类)19,788.00 KB
工作集 19,788.00 KB
私有工作集 7,900.00 KB
--------------------------------------------------------
符合预期,GC之后内存恢复到正常水平。
将上面的代码稍加修改,仅仅注释掉GC前面的一句代码:a.DelEvent();
getWorkingSet();
A a = new A();
TestInitEvent(a);
getWorkingSet(); Console.WriteLine("4,按下任意键开始注销事件,之后再次垃圾回收");
Console.ReadKey();
//a.DelEvent();
GC.Collect();
Console.WriteLine("5,垃圾回收完成");
getWorkingSet();
Console.ReadKey();
再看结果:
---------当前进程名称:ConsoleApplication1-----------
4424
工作集(进程类)10,308.00 KB
工作集 10,308.00 KB
私有工作集 7,040.00 KB
-------------------------------------------------------- ---------当前进程名称:ConsoleApplication1-----------
4424
工作集(进程类)121,256.00 KB
工作集 121,256.00 KB
私有工作集 7,592.00 KB
-------------------------------------------------------- 4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1-----------
4424
工作集(进程类)19,436.00 KB
工作集 19,436.00 KB
私有工作集 7,600.00 KB
--------------------------------------------------------
大跌眼镜:居然没有发生大量内存占用的情况!
看来只有一个可能性:
对象a 在GC回收内存之前,没有操作事件之类的代码,因此可以非常明确对象a 之前的事件代码不再有效,相关的对象b可以在 TestInitEvent(a); 方法调用之后立刻回收,这样就看到了现在的测试结果。
如果不是 Release 编译模式优化,我们来看看在IDE调试或者Debug编译模式运行的结果(前面的代码不做任何修改):
---------当前进程名称:ConsoleApplication1.vshost-----------
8260
工作集(进程类)25,148.00 KB
工作集 25,148.00 KB
私有工作集 9,816.00 KB
-------------------------------------------------------- ---------当前进程名称:ConsoleApplication1.vshost-----------
8260
工作集(进程类)136,048.00 KB
工作集 136,048.00 KB
私有工作集 112,888.00 KB
-------------------------------------------------------- 4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1.vshost-----------
8260
工作集(进程类)136,692.00 KB
工作集 136,692.00 KB
私有工作集 112,892.00 KB
--------------------------------------------------------
这一次,尽管仍然调用了GC垃圾回收,但实际上根本没有立刻起到效果,内存仍然100多M。
最后,我们在发起事件挂钩之后,立即解除事件挂钩,再看下Debug模式下的结果,为此仅仅需要修改下面代码一个地方:
static void TestInitEvent(A a)
{
var b = new B( * * );
a.ToDoSomething += b.PrintA;
//
a.ToDoSomething -= b.PrintA;
}
然后看在Debug模式下的执行结果:
---------当前进程名称:ConsoleApplication1.vshost-----------
8652
工作集(进程类)26,344.00 KB
工作集 26,344.00 KB
私有工作集 9,452.00 KB
-------------------------------------------------------- ---------当前进程名称:ConsoleApplication1.vshost-----------
8652
工作集(进程类)135,628.00 KB
工作集 135,628.00 KB
私有工作集 10,008.00 KB
-------------------------------------------------------- 4,按下任意键开始注销事件,之后再次垃圾回收
5,垃圾回收完成
---------当前进程名称:ConsoleApplication1.vshost-----------
8652
工作集(进程类)33,768.00 KB
工作集 33,768.00 KB
私有工作集 10,008.00 KB
--------------------------------------------------------
符合预期,内存占用量没有增加,所以此时调用GC回收内存都没有意义了。
疑问:
一定需要解除事件挂钩吗?
不一定,如果发起事件的对象生命周期比较短,不是静态对象,不是单例对象,当该对象生命周期结束的时候,GC可以回收该对象,只不过,该对象可能要经过多代才能成功回收,并且每一次回收何时才执行是不确定的,回收的代数越长,那么最后被回收的时间越长。
所以,如果发起事件的对象不是根对象,而是附属于另外一个生命周期很长的对象,不解除事件挂钩,这些处理事件的对象也不能被释放,于是内存泄漏就发生了。
为了避免潜在发生内存泄漏的问题,我们应该养成不使用事件就立刻解除事件挂钩的良好习惯!
需要在程序代码中常常写GC回收内存吗?
不一定,除非你非常清楚要在何时回收内存并且肯定此时GC能够有效工作,比如像本文测试的例子这样,否则,调用GC非但没有效果,可能还会引起副作用,比如引起整个应用程序的暂停业务处理。
总结
使用事件的时候如果不在使用完之后解除事件挂钩,有可能发生内存泄漏,
GC内存回收的时机的确具有不确定性,所以GC不是救命稻草,最佳的做法还是用完事件立即解除事件挂钩。
如果你忘记了这个事情,也请一定不要忘记发布程序的时候,使用Release编译模式!
Release编译模式下,事件是否会引起内存泄漏问题初步研究的更多相关文章
- ASIHTTPRequest 在release(打包)模式下数据获取或post失败问题
ASIHTTPRequest 在relase模式下失效 表现为,调用网络请求后没有任何反应 原因之一: ARC模式下,在ASIHTTPRequest 前面会加上__weak来解决循环应用,这个__we ...
- VS环境下C++如何检查是否内存泄漏
c++如何检查是否内存泄漏 今天在做OpenGL引擎的时候,突然想到检查一下内存泄漏.具体是我做了一个渲染类Render,将所有世界中存在的物体的指针都存放在这个类中.于是我不免担心,在Render中 ...
- xmake从入门到精通8:切换编译模式
xmake是一个基于Lua的轻量级现代化c/c++的项目构建工具,主要特点是:语法简单易上手,提供更加可读的项目维护,实现跨平台行为一致的构建体验. 本文我们会详细介绍下如何在项目构建过程中切换deb ...
- 基于Frida框架打造Art模式下的脱壳工具(OpenMemory)的原理分析
本文博客地址:https://blog.csdn.net/QQ1084283172/article/details/80956614 作者dstmath在看雪论坛公布一个Android的art模式下基 ...
- DEBUG模式下, 内存中的变量地址分析
测试函数的模板实现 /// @file my_template.h /// @brief 测试数据类型用的模板实现 #ifndef MY_TEMPLATE_H_2016_0123_1226 #defi ...
- DexHunter在Dalvik虚拟机模式下的脱壳原理分析
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78494671 在前面的博客<DexHunter的原理分析和使用说明(一)&g ...
- C语言:虚拟地址 和编译模式
所谓虚拟地址空间,就是程序可以使用的虚拟地址的有效范围.虚拟地址和物理地址的映射关系由操作系统决定,相应地,虚拟地址空间的大小也由操作系统决定,但还会受到编译模式的影响.这节我们先讲解CPU,再讲解编 ...
- soui edit passwrod模式下禁用输入法
一直在用soui做客户端界面,今天发现密码edit在中文输入法下不能输入密码.我在想难道不是这样吗,密码就该用英文输入法啊. 然后我就用mfc的做了个demo,发现mfc的edit在密码模式下是可以用 ...
- 如何在linux下检测内存泄漏
之前的文章应用 Valgrind 发现 Linux 程序的内存问题中介绍了利用Linux系统工具valgrind检测内存泄露的简单用法,本文实现了一个检测内存泄露的工具,包括了原理说明以及实现细节. ...
随机推荐
- 安装指定版本的cordova
安装指定版本的cordova 刚接触cordova看到教程肯定是直接 npm install -g cordova 然后下载个集成的adt 以为万事大吉,开始hello world 玩玩没有想到最新的 ...
- 被废了的display:box弹性盒模型
这几天在研究弹性布局,看书中写的是display:box,结果在chrome浏览器中是正常的,想着移动端大部分浏览器也是webkit内核的应该也没啥问题,结果确实没问题,但仔细一看,高度呢?好吧,严重 ...
- 深入理解CSS中的空白符和换行
前面的话 CSS3新增了两个换行属性word-wrap和word-break.把空白符和换行放在一起说,是因为实际上空白符是包括换行的,且常用的文本不换行是使用的空白符的属性white-space: ...
- jQuery.validationEngine.js学习
项目中使用到了这个插件,抽了个空,看了一下. (function($){ var method ={} $.fn.validationEngine = function(){} $.validatio ...
- tomcat匹配Servlet的流程(原创)
- ISTool5.3.1汉化版使用教程
ISTool是帮助你创建由Jordan Russell制作的Inno Setup编译器脚本的工具.Inno是一个出色的编译器,即使对于某些专业的安装制作程序,它唯一的缺点就是它的脚本必须手动编写. 这 ...
- javascript学习6
JavaScript Boolean(逻辑)对象 Boolean(逻辑)对象用于将非逻辑值转换为逻辑值(true 或者 false). 实例 检查逻辑值 检查逻辑对象是 true 还是 false. ...
- Azure Redis Cache (3) 创建和使用P级别的Redis Cache
<Windows Azure Platform 系列文章目录> 在笔者之前的文档里面已经说明了,Azure Redis Cache分为三个不同的级别: - 基本,Basic,不包含SLA ...
- mysql插入日期 vs oracle插入日期
今天做oracle日期插入的时候突然开始疑惑日期是如何插入的. 用框架久了,反而不自己做简单的工作了.比如插入. 通常,新建一个表对象,然后绑定数据,前端form提交,后端getModel后直接mod ...
- .Net语言 APP开发平台——Smobiler学习日志:如何仿微信朋友圈的消息样式?
最前面的话:Smobiler是一个在VS环境中使用.Net语言来开发APP的开发平台,也许比Xamarin更方便 一.目标样式 我们要实现上图中的效果,需要如下的操作: 1.从工具栏上的”Smobil ...