题记:不常发生的事件内存泄漏现象

想必有些朋友也常常使用事件,但是很少解除事件挂钩,程序也没有听说过内存泄漏之类的问题。幸运的是,在某些情况下,的确不会出问题,很多年前做的项目就跑得好好的,包括我也是,虽然如此,但也不能一直心存侥幸,总得搞清楚这类内存泄漏的神秘事件是怎么发生的吧,我们今天可以做一个实验来再次验证下。

可以,为了验证这个问题,我一度怀疑自己代码写错了,甚至照着书上(网上)例子写也无法重现事件引起内存泄漏的问题,难道教科书说错了么?

首先来看看我的代码,先准备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 = 0; i < size ; i++)data[i] = 0;}public  void PrintA(object sender, EventArgs e){((A)sender).Print("sender:"+ sender.GetType ());}}

然后,在主程序里面写下面的方法:

        static void TestInitEvent(A a){var b = new B(100 * 1024 * 1024);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 / 1024);Console.WriteLine("{0}{1:N} KB", "工作集 ", process.WorkingSet64 / 1024);// process.PrivateMemorySize64 私有工作集 不是很准确,大概多9M Console.WriteLine("{0}{1:N} KB", "私有工作集 ", p1.NextValue() / 1024); //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();

结果:

2,按下任意键,主对象发起事件
A:sender:ConsoleApplication1.A
---------当前进程名称:ConsoleApplication1-----------
7056
工作集(进程类)121,576.00 KB
工作集 121,576.00 KB
私有工作集 109,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模式),来看看执行结果:

 View Code

这次的确印证了前面的说明,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(100 * 1024 * 1024);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编译模式!

本文转自深蓝医生博客园博客,原文链接:http://www.cnblogs.com/bluedoctor/p/5268615.html,如需转载请自行联系原作者

Release编译模式下,事件是否会引起内存泄漏问题初步研究相关推荐

  1. weex android 滑动事件,【报Bug】weex编译模式下slider组件 @scroll 事件, 滑块左右滑动, @scroll 回调的值始终是负数, 判断不了左右动作...

    详细问题描述 weex编译模式下slider组件 @change事件, 滑块左右滑动, @change回调的值始终是负数, 判断不了左右动作 weex官方文档 (DCloud产品不会有明显的bug,所 ...

  2. 切换Debug/Release编译模式和Archive的作用

    在学这个之前,以为很难,也起不到什么作用,但是等真正运用到工程里面,才发现,这个能帮你省下很多工作量. 1,Debug和Release版本区别? 进行iOS开发,在Xcode调试程序时,分为两种方式 ...

  3. 虚拟地址空间以及编译模式

    原文链接 虚拟地址空间以及编译模式 < 上一页虚拟内存到底是什么?为什么我们在C语言中看到的地址是假的? C语言内存对齐,提高寻址效率下一页 > 所谓虚拟地址空间,就是程序可以使用的虚拟地 ...

  4. C++ 函数模板与分离编译模式

    代码编译运行环境:VS2017+Debug+Win32 1.分离编译模式 一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程 ...

  5. php 如何执行top命令,技术|如何在批处理模式下运行 top 命令

    top 命令 是每个人都在使用的用于 监控 Linux 系统性能 的最好的命令.你可能已经知道 top 命令的绝大部分操作,除了很少的几个操作,如果我没错的话,批处理模式就是其中之一. 大部分的脚本编 ...

  6. OFDM子载波频率 知乎_5G新在哪儿(14)-NSA模式下测量机制与系统间互操作

    欢迎关注微信公众号:网优小谈 在网络建设初期,由于建设进度和投资等诸多因素可能在某些局部区域造成网络覆盖空洞或者弱覆盖,为了保证用户使用移动通信网络的用户感知,需要通过将新建通信基础网络与已有的通信基 ...

  7. Keil的三种编译模式:smal、compact、large

    收藏 评论(0) 分享到 微博 QQ 微信 LinkedIn Keil Cx51编译器提供三条编译模式控制命令:SMALL,COMPACT,LARGE,它们对变量存储器空间的影响如下. SMALL:所 ...

  8. vue几种编译_nvue不同编译模式介绍

    HBuilderX 自 2.0.3 版本开始,nvue文件同时支持两种编译模式: weex 模式:老模式,使用 weex组件,写法同weex标准写法.只能在 App 端中运行,部分 uni-app J ...

  9. 第19章 归档模式下的数据库恢复

    1. Restore(修复)将数据文件带回到过去(备份的时间点)+Recover(恢复)恢复从备份到数据文件崩溃这段时间内所有提交的数据=>数据库的完全恢复(所有提交的数据都恢复): 1)修复损 ...

最新文章

  1. 1370亿参数、接近人类水平,谷歌重磅推出对话AI模型LaMDA
  2. WordPress设计bug+WooCommerce漏洞导致网站存在被劫持风险
  3. win7系统下配置openCV python环境附加 numpy +scipy安装
  4. 算法学习之路|打印排名
  5. 【线上分享】下一代互联网通讯协议:QUIC
  6. linux 物理内存用完了_12张图解Linux内存管理,程序员内功修炼,看过都说懂了!...
  7. JavaSE08:详解多线程(超详细)
  8. UGUI的InputField
  9. java代码生成器,springboot代码生成器—增加生成添加信息,导出excel功能(持续更新)
  10. linux中实现getch函数
  11. 泛微协同办公系统移动服务器,泛微协同办公平台Ecology系统重装迁移指导手册.pdf...
  12. 【中间件技术】第四部分 Web Service规范(10) Web Service规范
  13. java web 测试要点记录
  14. 数据预处理阶段“不处理”缺失值的思路
  15. 与MySQL的纠缠(卸载与安装)
  16. C语言真的很难吗?那是你没看这张图,化整为零轻松学习C语言。
  17. correl函数_【Excel函数】CORREL函数 - 曹海峰个人博客
  18. 阿里云服务器中安装配置MYSQL数据库完整教程
  19. java毕业设计——基于java+Java awt+swing的愤怒的小鸟游戏设计与实现(毕业论文+程序源码)——愤怒的小鸟游戏
  20. 【jquery事件】

热门文章

  1. 2021汉语言文学对高考成绩查询,2021汉语言文学专业就业前景怎么样
  2. 同一主机的多个子进程使用同一个套接字_在操作系统中进程是如何通信的
  3. layui table动态选中_layui后台管理—table 数据表格详细讲解
  4. Go bufio.Reader 结构+源码详解
  5. SPA (单页应用程序)
  6. java树洞_SSM框架开发案例——铁大树洞后台管理系统
  7. Java校招笔试题-Java基础部分(三)
  8. Rust性能分析-迭代器的enumerate方法是否会影响程序性能
  9. vSwitch报文转发分析
  10. Netty Reactor线程模型与EventLoop详解