C#内存泄漏--event内存泄漏

内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被GC视为垃圾进行回收。这块内存会一直存在,直到程序退出。C#是托管型代码,其内存的分配和释放都是由CLR负责,当一块内存没有任何实例引用时,GC会负责将其回收。既然没有任何实例引用的内存会被GC回收,那么内存泄漏是如何发生的?

内存泄漏示例

  为了演示内存泄漏是如何发生的,我们来看一段代码

复制代码

class Program

{

static event Action TestEvent;

static void Main(string[] args)

{

var memory = new TestAction();

TestEvent += memory.Run;

OnTestEvent();

memory = null;

//强制垃圾回收

GC.Collect(GC.MaxGeneration);

Console.WriteLine("GC.Collect");

//测试是否回收成功

OnTestEvent();

Console.ReadLine();

}

public static void OnTestEvent() {

if (TestEvent != null) TestEvent();

else Console.WriteLine("Test Event is null");

}

class TestAction
{
public void Run() {
Console.WriteLine("TestAction Run.");
}
}

}

复制代码

  该例子中,memory.run订阅了TestEvent事件,引发事件后,会在屏幕上看到 TestAction Run。当memory =null 后,memory原来指向的内存就没有任何实例再引用该块内存了,这样的内存就是待回收的内存。GC.Collect(GC.MaxGeneration)语句会强制执行一次垃圾回收,再次引发事件,发现屏幕上还是会显示TestAction Run。该内存没有被GC回收,这就是内纯泄漏。这是由TestEvent+=memory.Run语句引起的,当GC.Collect执行的时候,当他看到该块内存还有TestEvent引用,就不会进行回收。但是该内存已经是“无法到达”的了,即无法调用该块内存,只有在引发事件的时候,才能执行该内存的Run方法。这显然不是我想要的效果,当memory = null执行时,我希望该内存在GC执行时被回收,并且当TestEvent被引发时,Run方法不会执行,因为我已经把该内存“解放”了。

  这里有一个问题,就是C#中如何“释放”一块内存。像C和C++这样的语言,内存的声明和释放都是开发人员负责的,一旦内存new了出来,就要delete,不然就会造成内存泄漏。这更灵活,也更麻烦,一不小心就会泄漏,忘记释放、线程异常而没有执行释放的代码...有手动分配内存的语言就有自动分配和释放的语言。最开始使用垃圾回收的语言是LISP,之后被用在Java和C#等托管语言中。像C#,CLR负责内存的释放,当程序执行一段时间后,CLR检测到垃圾内存已经值得进行一次垃圾回收时,会执行垃圾回收。至于如何判定一块内存是否为垃圾内存,比较著名的是计数法,即有一个实例引用了该内存后,就在该内存的计数上+1,改实例取消了对该内存的引用,计数就-1,当计数为0时,就被判定为垃圾。该种方法的问题是对循环引用束手无策,如A的某个字段引用了B,而B的某个字段引用了A,这样A和B的技术都不会降到0。CLR改用的方法是类似“标记引用法”(我自己的命名):在执行GC时,会挂起全部线程,并将托管堆中所有的内存都打上垃圾的标记,之后遍历所有可到达的实例,这些实例如果引用了托管堆的内存,就将该内存的标记由垃圾变为被引用。当遇到A和B相互引用的时候,如果没有其他实例引用A或者B,虽然A和B相互引用,但是A和B都是不可到达的,即没办法引用A或者B,则A和B都会被判定为垃圾而被回收。讲解了这么一大堆,目的就是要说,在C#中,你想要释放一块内存,你只要让该块内存没有任何实例引用他,就可以了。那么当执行memory = null后,除了对TestEvent的订阅,没有任何实例再引用了该块内存,那么为什么订阅事件会阻止内存的释放?

  我们来看看TestEvent+=memory.Run()这句话都干了什么。我们利用IL反编译上面的dll,可以看到

复制代码

1 IL_0000: nop

2 IL_0001: newobj instance void EventLeakMemory.Program/TestAction::.ctor()

3 IL_0006: stloc.0

4 IL_0007: ldloc.0

5 IL_0008: ldftn instance void EventLeakMemory.Program/TestAction::Run()

6 IL_000e: newobj instance void [mscorlib]System.Action::.ctor(object, native int)

7 IL_0013: call void EventLeakMemory.Program::add_TestEvent(class [mscorlib]System.Action)

...//其他部分

复制代码

  关键在5-7行。第5和6行,声明了一个System.Action型的委托,参数为TestAction.Run方法,第七行,执行了Program.add_TestEvent方法,参数是上面声明的委托。也就是说+=操作符相当于执行了Add_TestEvent(new Action(memory.Run)),就是这个new Action包含了对memory指向的内存的引用。而这个引用在CLR看来是可达的,可以通过引发事件来调用该内存。

解决办法

  我们已经找到了内存泄漏的元凶,就是订阅事件时,隐式声明的匿名委托对内存的引用。最简单的解决办法是手动取消订阅事件,只要TestEvent -= memory.Run就可以了。但如何实现一个不需要手动取消订阅的事件?该问题的解决办法是使用一种和普通的引用不同的方式来引用方法的实例对象:该引用不会影响垃圾回收,不会在GC时被判定为对该内存的引用,也就是“弱引用”。C#中,绝大部分的类型都是强引用。如何实现弱引用?来看一个例子:

复制代码

static void Main(string[] args){

var obj = new object();

var gcHandle = GCHandle.Alloc(obj, GCHandleType.Weak);

Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null);

obj = null;

GC.Collect();

Console.WriteLine("GC.Collect");

Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null);

Console.ReadLine();

}

复制代码

  当执行GC。Collect后,gcHandle.Target == null 由false 变成了true。这个gcHandle就是obj的一个弱引用。这个类的详细介绍见 GCHandle 。比较关键的是GCHandle.Alloc方法的第二个参数,该参数接受一个枚举类型。我使用的是GCHandleType.Weak,表明该引用是个弱引用。利用这个方法,就可以封装一个自己的WeakReference类,代码如下:

  我实现了IEquatable接口,该接口能方便的比较WeakReference实例和委托是否指一个方法。利用该类,就可以写一个自己的弱事件封装器。

  最后,就可以像下面这样定义自己的事件了

复制代码

public class TestEventClass {

private WeakEventManager<Action<object, EventArgs>> _testEvent = new WeakEventManager<Action<object, EventArgs>>();

public event Action<object, EventArgs> TestEvent {

add { _testEvent.AddHandler(value); }

remove { _testEvent.RemoveHandler(value); }

}

protected virtual void OnEvent(EventArgs e) {
_testEvent.Raise(this, e);
}

}

复制代码

  这里,要感谢@delowly网友的提醒,告诉我代码有错误,达不到效果。这个简易的弱事件是改版的,老版本由于我没有测试代码,就放到了文章中,造成了错误,对此深表歉意。我保证以后所有的文章中写的代码都要经过反复的测试。

event内存泄漏的更多相关文章

  1. event 内存泄漏

    组长说用event有内存泄漏的隐患..做个测试. 预留

  2. C#内存泄漏--event内存泄漏

    内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被GC视为垃圾进行回收.这块内存会一直存在,直到程序退出.C#是托管型代码,其内存的分配和释放都是由CLR负责 ...

  3. Android性能优化之利用Rxlifecycle解决RxJava内存泄漏

    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学 ...

  4. .net中事件引起的内存泄漏分析

    系列主题:基于消息的软件架构模型演变 在Winform和Asp.net时代,事件被大量的应用在UI和后台交互的代码中.看下面的代码: private void BindEvent() { var bt ...

  5. 浅析c#内存泄漏

    一直以来都对内存泄露和内存溢出理解的不是很深刻.在网上看到了几篇文章,于是整理了一下自己对内存泄露和内存溢出的理解. 一.概念 内存溢出:指程序在运行的过程中,程序对内存的需求超过了超过了计算机分配给 ...

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

    题记:不常发生的事件内存泄漏现象 想必有些朋友也常常使用事件,但是很少解除事件挂钩,程序也没有听说过内存泄漏之类的问题.幸运的是,在某些情况下,的确不会出问题,很多年前做的项目就跑得好好的,包括我也是 ...

  7. js内存泄漏

    IE和webkit浏览器都是采用计数来处理垃圾,也就是说每个对象被引用一次,该对象的计数器成员+1,如果计数器为0,那么这个对象被销毁 例如: function A() { var obj = {}; ...

  8. UWP开发入门(十六)——常见的内存泄漏的原因

    本篇借鉴了同事翔哥的劳动成果,在巨人的肩膀上把稿子又念了一遍. 内存泄漏的概念我这里就不说了,之前<UWP开发入门(十三)——用Diagnostic Tool检查内存泄漏>中提到过,即使有 ...

  9. UWP开发入门(十三)——用Diagnostic Tool检查内存泄漏

    因为.NET的垃圾回收机制相当完善,通常情况下我们是不需要关心内存泄漏的.问题人一但傻起来,连自己都会害怕,几个页面跳啊跳的,内存蹭蹭的往上涨,拉都拉不住.这种时候我们就需要冷静下来,泡一杯热巧克力. ...

随机推荐

  1. How to remove focus without setting focus to another control?

    How to remove focus without setting focus to another control? Ask Question up vote 67 down vote favo ...

  2. [POJ 2279] Mr. Young's Picture Permutations

    [题目链接] http://poj.org/problem?id=2279 [算法] 杨氏矩阵与勾长公式 [代码] #include <algorithm> #include <bi ...

  3. BZOJ 1369 树形DP

    思路: f[i][j] 表示节点i 染成j时 子树的最小权值 (我会猜这个j很小 你打我吖~) 随便DP一发就好了 (证明我也不会) //By SiriusRen #include <cstdi ...

  4. [转]C# 位域[flags]

    .NET中的枚举我们一般有两种用法,一是表示唯一的元素序列,例如一周里的各天:还有就是用来表示多种复合的状态.这个时候一般需要为枚举加上[Flags]特性标记为位域,例如: [Flags]   enu ...

  5. FluentAPI配置

    基本 EF 配置只要配置实体类和表.字段的对应关系.表间关联关系即可. 如何利用 EF的高级配置,达到更多效果:如果数据错误(比如字段不能为空.字符串超长等),会在 EF 层就会报错,而不会被提交给数 ...

  6. 手把手教你写带登录的NodeJS爬虫+数据展示

    其实在早之前,就做过立马理财的销售额统计,只不过是用前端js写的,需要在首页的console调试面板里粘贴一段代码执行,点击这里.主要是通过定时爬取https://www.lmlc.com/s/web ...

  7. jmeter的认识——线程组的认识

    名称:可以给线程组设置一个个性化的命名 注释:可以对线程组添加备注以标记 在取样器错误后要执行的动作:就是在错误之后要如何执行,可选继续执行后续的.停止执行等. 线程数:就是需要设置多少线程执行测试. ...

  8. mysql+spring+mybatis实现数据库读写分离[代码配置] .

    场景:一个读数据源一个读写数据源. 原理:借助spring的[org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource] ...

  9. Imperative programming

    In computer science, imperative programming is a programming paradigm that uses statements that chan ...

  10. 使用Git--将本地项目提交到Github

    前置工作 1. 在GitHub官网注册一个GitHub账号: 2. 安装git工具,在Git官网下载对应版本的Git: 方法一: 1. 进入Github首页,点击New repository新建一个项 ...