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.0
IL_0007: ldloc.0
IL_0008: ldftn  instance void EventLeakMemory.Program/TestAction::Run()
IL_000e: newobj  instance void [mscorlib]System.Action::.ctor(object, native int)
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看来是可达的,可以通过引发事件来调用该内存。

解决办法

我们已经找到了内存泄漏的元凶,就是订阅事件时,隐式声明的匿名委托对内存的引用。该问题的解决办法是使用一种和普通的引用不同的方式来引用方法的实例对象:该引用不会影响垃圾回收,不会在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<T> where T : class {
 private GCHandle handle;

 public WeakReference(T obj) {
  if (obj == null) return;
  handle = GCHandle.Alloc(obj, GCHandleType.Weak);
 }

 /// <summary>
 /// 引用的目标是否还存活(没有被GC回收)
 /// </summary>
 public bool IsAlive {
  get {
   if (handle == default(GCHandle)) return false;
   return handle.Target != null;
  }
 }

 /// <summary>
 /// 引用的目标
 /// </summary>
 public T Target {
  get {
   if (handle == default(GCHandle)) return null;
   return (T)handle.Target;
  }
 }
}

利用该类,就可以写一个自己的弱事件封装器。

public class WeakEventManager<T> {
 private Dictionary<Delegate, WeakReference<T>> delegateDictionary;

 public WeakEventManager() {
  delegateDictionary = new Dictionary<Delegate, WeakReference<T>>();
 }

 /// <summary>
 /// 订阅
 /// </summary>
 public void AddHandler(Delegate handler) {
  if (handler != null)
   delegateDictionary[handler] = new WeakReference<T>(handler);
 }

 /// <summary>
 /// 取消订阅
 /// </summary>
 public void RemoveHandler(Delegate handler) {
  if (handler != null)
   delegateDictionary.Remove(handler);
 }

 /// <summary>
 /// 引发事件
 /// </summary>
 public void Raise(object sender, EventArgs e) {
  foreach (var key in delegateDictionary.Keys) {
   if (delegateDictionary[key].IsAlive)
    key.DynamicInvoke(sender, e);
   else
    delegateDictionary.Remove(key);
  }
 }
}

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

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);
 }
}

您可能感兴趣的文章:

  • C#中event内存泄漏总结
  • C#事件(event)使用方法详解
  • C# ManualResetEvent使用方法详解
  • C#中ManualResetEvent用法详解
  • C# 中的EventHandler实例详解
  • C#使用AutoResetEvent实现同步
  • C#3.0使用EventLog类写Windows事件日志的方法
  • C#采用mouse_event函数实现模拟鼠标功能
  • C#事件处理和委托event delegate实例简述
  • C# 使用匿名函数解决EventHandler参数传递的难题
(0)

相关推荐

  • C#中event内存泄漏总结

    内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被GC视为垃圾进行回收.这块内存会一直存在,直到程序退出.C#是托管型代码,其内存的分配和释放都是由CLR负责,当一块内存没有任何实例引用时,GC会负责将其回收.既然没有任何实例引用的内存会被GC回收,那么内存泄漏是如何发生的? 内存泄漏示例 为了演示内存泄漏是如何发生的,我们来看一段代码 class Program { static event Action TestEvent; static void

  • C#使用AutoResetEvent实现同步

    前几天碰到一个线程的顺序执行的问题,就是一个异步线程往A接口发送一个数据请求.另外一个异步线程往B接口发送一个数据请求,当A和B都执行成功了,再往C接口发送一个请求.说真的,一直做BS项目,对线程了解,还真不多.就知道AutoResetEvent这个东西和线程有关,用于处理线程切换之类,于是决定用AutoResetEvent来处理上面的问题. 于是网上查找相关资料: 原来,AutoResetEvent在.Net多线程编程中经常用到.当某个线程调用WaitOne方法后,信号处于发送状态,该线程会得

  • C# ManualResetEvent使用方法详解

    本文实例为大家分享了ManualResetEvent的使用方法,供大家参考,具体内容如下 1. 源码下载: 下载地址:ManualResetEvent Demo: 2. ManualResetEvent详解 ManualResetEvent 允许线程通过发信号互相通信.通常,此通信涉及一个线程在其他线程进行之前必须完成的任务.当一个线程开始一个活动(此活动必须完成后,其他线程才能开始)时,它调用 Reset 以将 ManualResetEvent 置于非终止状态,此线程可被视为控制 Manual

  • C#3.0使用EventLog类写Windows事件日志的方法

    本文实例讲述了C#3.0使用EventLog类写Windows事件日志的方法.分享给大家供大家参考.具体如下: 在程序中经常需要将指定的信息(包括异常信息和正常处理信息)写到日志中.在C#3.0中可以使用EventLog类将各种信 息直接写入Windows日志.EventLog类在System.Diagnostics命名空间中.我们可以在"管理工具" > "事件查看器"中 可以查看我们写入的Windows日志 下面是一个使用EventLog类向应用程序(App

  • C#事件处理和委托event delegate实例简述

    本文实例讲述了C#事件处理和委托event delegate,分享给大家供大家参考.具体方法如下: 以下仅仅是用最简单的方式表示事件,实际应用可能是不同窗体之间相互通知某些操作,达到触发. 首先声明一个degate的 EventHandler 参数可以没有 一个或多个 但是触发和使用一定要匹配. 创建一个该EvenHandler的实例a 在程序建立或你需要的时候产生一个事件触发申明: a += new EventHandler(d); public delegate void EventHand

  • C#事件(event)使用方法详解

    事件(event),这个词儿对于初学者来说,往往总是显得有些神秘,不易弄懂.而这些东西却往往又是编程中常用且非常重要的东西.大家都知道windows消息处理机制的重要,其实C#事件就是基于windows消息处理机制的,只是封装的更好,让开发者无须知道底层的消息处理机制,就可以开发出强大的基于事件的应用程序来. 先来看看事件编程有哪些好处. 在以往我们编写这类程序中,往往采用等待机制,为了等待某件事情的发生,需要不断地检测某些判断变量,而引入事件编程后,大大简化了这种过程: - 使用事件,可以很方

  • C#采用mouse_event函数实现模拟鼠标功能

    下面我通过代码为大家分享下C#模拟鼠标,具体内容如下: 想必有很多人在项目开发中可能遇见需要做模拟鼠标点击的小功能,很多人会在百度过后采用mouse_event这个函数,不过我并不想讨论如何去使用mouse_event函数怎么去使用,因为那没有多大意义. static void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo) { int x = dx, y = dy; edit_position(dw

  • C#中ManualResetEvent用法详解

    第一.简单介绍 ManualResetEvent 允许线程通过发信号互相通信.通常,此通信涉及一个线程在其他线程进行之前必须完成的任务.当一个线程开始一个活动(此活动必须完成后,其他线程才能开始)时,它调用 Reset 以将 ManualResetEvent 置于非终止状态,此线程可被视为控制 ManualResetEvent.调用 ManualResetEvent 上的 WaitOne 的线程将阻止,并等待信号. 当控制线程完成活动时,它调用 Set 以发出等待线程可以继续进行的信号.并释放所

  • C# 中的EventHandler实例详解

    废话不多说了,具体详情如下所示: //这里定义了一个水箱类 public class 水箱 { //这是水箱的放水操作 public void 放水() { } //这是水箱的属性 public double 体积; //这是水箱空的事件 public event EventHandler 水箱空; } //这里定义了一个加水器类 public class 加水器 { public void 加水(Object sender, EventArgs e) { //对需要加水的水箱进行加水操作 }

  • C# 使用匿名函数解决EventHandler参数传递的难题

    首先,动态生成PictureBox,很简单, PictureBox box = new PictureBox() ; box.ImageLocation = imageRoad ; 其次,给PictureBox添加右键菜单,也不难, ContextMenu menu = new ContextMenu(); box.ContextMenu = menu ; 然后,要给右键菜单增加"删除"项,并实现删除图片事件.这个,比较麻烦. MenuItem item = new MenuItem(

随机推荐