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,可以看到
IL_0000: nop
IL_0001: newobj instance void EventLeakMemory.Program/TestAction::.ctor()
IL_0006: stloc.
IL_0007: ldloc.
IL_0008: ldftn instance void EventLeakMemory.Program/TestAction::Run()
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类,代码如下:
public class WeakReference<TDelegate> : IEquatable<Delegate> {
private GCHandle _handle; public WeakReference(Delegate obj) {
if (obj == null) return;
_handle = GCHandle.Alloc(obj, GCHandleType.Weak);
} /// <summary>
/// 引用的目标是否还存活(没有被GC回收)
/// </summary>
public bool IsAlive {
get { return _handle != default(GCHandle) && _handle.Target != null; }
} /// <summary>
/// 引用的目标
/// </summary>
public TDelegate Target {
get {
if (_handle == default(GCHandle)) return default(TDelegate);
return (TDelegate)_handle.Target;
}
} /// <summary>
/// 实现接口,方便与委托的比较
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(Delegate other) {
return _handle != default(GCHandle) && other != null &&
((Delegate)_handle.Target).Method.Equals(other.Method);
} /// <summary>
/// 释放弱引用
/// </summary>
~WeakReference() {
_handle.Free();
}
}
我实现了IEquatable<Delegate>接口,该接口能方便的比较WeakReference实例和委托是否指一个方法。利用该类,就可以写一个自己的弱事件封装器。
public class WeakEventManager {
private readonly List<WeakReference<Delegate>> _delegateList; public WeakEventManager() {
_delegateList = new List<WeakReference<Delegate>>();
} /// <summary>
/// 订阅
/// </summary>
public void AddHandler(Delegate handler) {
if (handler != null)
_delegateList.Add(new WeakReference<Delegate>(handler));
} /// <summary>
/// 取消订阅
/// </summary>
public void RemoveHandler(Delegate handler) {
if (handler == null) return;
//由于我实现了IEquatable<Delegate>,这里能够很方便的比较
var sameHandler = _delegateList.FirstOrDefault(e => e.Equals(handler));
if (sameHandler != null)
_delegateList.Remove(sameHandler);
} /// <summary>
/// 引发事件
/// </summary>
public void Raise(object sender, EventArgs e) {
foreach (var d in _delegateList.ToList()) {
if (d.IsAlive)
d.Target.DynamicInvoke(sender, e);
else
_delegateList.Remove(d);
}
}
}
最后,就可以像下面这样定义自己的事件了
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网友的提醒,告诉我代码有错误,达不到效果。这个简易的弱事件是改版的,老版本由于我没有测试代码,就放到了文章中,造成了错误,对此深表歉意。我保证以后所有的文章中写的代码都要经过反复的测试。
针对评论区@delowly的评论,我看了它代码,它的和我的最大的不同是在WeakReference没有实现IEquatable,要取消订阅,必须要调用WeakReference的实例,很是不方便。
与人分享自己的想法的感觉很棒,我会在以后的日子里写更多个人的心得,来和大家分享。欢迎在评论区与我交流。
C#内存泄漏--event内存泄漏的更多相关文章
- event内存泄漏
C#内存泄漏--event内存泄漏 内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被GC视为垃圾进行回收.这块内存会一直存在,直到程序退出.C#是托管型代码 ...
- 【面试题】如何让C语言自动发现泄漏的内存
1. 题目 改造malloc和free函数,使C语言能自动发现泄漏的内存,在程序退出时打印中遗漏的内存地址和大小. 2. 思路 用一个链表来记录已经分配的内存地址.在malloc时,把分配的内存地址和 ...
- JAVA 内存泄漏与内存溢出
一.Java内存回收机制 不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址.Java中对象是采用new或者反射或者clone或者反序列化的方法创建的, 这 ...
- (转)从内存管 理、内存泄漏、内存回收探讨C++内存管理
http://www.cr173.com/html/18898_all.html 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟 ...
- java中的内存溢出和内存泄漏
内存溢出:对于整个应用程序来说,JVM内存空间,已经没有多余的空间分配给新的对象.所以就发生内存溢出. 内存泄露:在应用的整个生命周期内,某个对象一直存在,且对象占用的内存空间越来越大,最终导致JVM ...
- java内存泄漏与内存溢出
https://www.cnblogs.com/panxuejun/p/5883044.html 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out o ...
- 内存溢出和内存泄漏的区别(ZZ)
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出 ...
- Android App卡顿慢优化之解决内存抖动及内存泄漏
前面一篇博客说到了,内存抖动的第二种情况,就是必须在短时间内创建对象,但是要控制数量:这个问题目前可以使用对象池的方法解决. 3)Object Pools 在程序里面经常会遇到的一个问题是短时间内创建 ...
- Android App解决卡顿慢之内存抖动及内存泄漏(发现和定位)
内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(很重要)在循环里创建对象(导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在 ...
随机推荐
- 【html5】html5学习笔记2--表单
html5 新增输入类型 html5新增了 email.number.Date pickers (date, month, week, time, datetime, datetime-local) ...
- unix时间戳转换成标准时间(c#)
//---unix时间戳转换成标准时间(c#)---// /* string timeStamp = "1144821796"; DateTime dtSt ...
- 笔记:promise实例+注释
////////////////////////////////////////////// var data = [1,2,3,4]; var promise = new Promise((reso ...
- CSS开启硬件加速提高网站性能
国外一篇文章,有点意思,转载过来,准备尝试下~ 中文地址:http://www.cnblogs.com/yzw7489757/ 原文地址:http://blog.teamtreehouse.com/i ...
- 标注-隐马尔可夫模型HMM的探究
1 HMM基本概念1.1 定义1.2 观测序列生成过程1.3 HMM的三个问题2 概率计算算法2.1 直接计算算法2.2 前向算法forward algorithm2.3 后向算法2.4 一些概率与期 ...
- C++堆栈详解
一.预备知识-程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)- 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. ...
- 论文笔记(5):Fully Convolutional Multi-Class Multiple Instance Learning
这篇论文主要介绍了如何使用图片级标注对像素级分割任务进行训练.想法很简单却达到了比较好的效果.文中所提到的loss比较有启发性. 大体思路: 首先同FCN一样,这个网络只有8层(5层VGG,3层全卷积 ...
- 【MyBatis源码分析】Configuration加载(上篇)
config.xml解析为org.w3c.dom.Document 本文首先来简单看一下MyBatis中将config.xml解析为org.w3c.dom.Document的流程,代码为上文的这部分: ...
- ES6学习总结二(数组的四个方法,字符串)
数组 1 map 映射 一个对一个 如:分数数组[34,56,78,99]映射为[不及格,不及格,及格,及格]; 等级数组[23,56,89]映射为 [ {name:'lmx',level:1,rol ...
- spring中的aop的xml配置方式简单实例
aop,即面向切面编程,面向切面编程的目标就是分离关注点,比如:一个骑士只需要关注守护安全,或者远征,而骑士辉煌一生的事迹由谁来记录和歌颂呢,当然不会是自己了,这个完全可以由诗人去歌颂,比如当骑士出征 ...