• 事件(Event),绝大多数内存泄漏(Memory Leak)的元凶[下篇] (提供Source Code下载)


    上篇中我们谈到:将一个生命周期较短的对象(对象A)注册到一个生命周期较长(对象B)的某个事件(Event)上,两者便无形之间建立一个引用关系(B引用A)。这种引用关系导致GC在进行垃圾回收的时候不会将A是为垃圾对象,最终使其常驻内存(或者说将A捆绑到B上,具有了和B一样的生命周期)。这种让无用的对象不能被GC垃圾回收的现象,在托管环境下就是一种典型的内存泄漏问题。我们今天将会着重解释其背后的原因。[本篇文章的Source Code从这里下载)

    一、CLR垃圾回收简介

    在一个托管应用程序中,我们通过不同的方式创建一个托管对象(比如通过new关键字、反射或反序列化等)时,CLR会在托管堆为该对象开辟一块内存空间。对象的本质就是存储于某块内存中数据的体现,对象的生命周期终止于相应内存被回收之时。对于CLR来说,负责对托管堆(在这里主要指GC堆)进行回收的组件是垃圾收集器(GC),GC掌握着托管对象的生杀大权,决定着托管对象的生命周期。

    当GC在进行垃圾回收的时候,会将“无用”的对象标记为垃圾对象,然后再对垃圾对象进行清理。GC对“无用”对象的识别机制很简单:判断对象是否被“根(Root)”所引用。在这里,“根”是对一组当前正被使用,或者以后可能被使用的对象的统称,大体包括这样的对象:类型的静态字段或当前的方法参数和局部变量、CPU寄存器等。

    所以,孤立存在的对象将难逃被GC回收的厄运。反之,如果希望某个对象常驻内存中,我们唯一的方式就是通过某个“根”引用该对象。如果想让对象实例按照我们希望的方式创建、存活和消亡,所以我们唯一的方式也只能是:在希望它存活的时候让它被某个“根”引用,从而阻止GC将其回收;在希望它被回收的时候连“根”去除,使GC能够将其回收。

    二、关于事件(Event)那点事

    简单介绍了CLR的垃圾回收机制,我们再来谈谈关于事件的话题。我们知道,事件本质上就是一个System.Delegate对象。Delegate是一个特别的对象,我们单从语意上去来理解Delegate:Delegate的中文翻译是“代理”,意思是委托某人做某事。比如说,我请某人作为我们的代理律师打官司,就是一个很好的Delegate的例子。仔细分析我举的这个例子,我们可以将一个Delegate分解成两个部分:委托的事情(打官司)和委托的对象(某个律师)。与之相似地,.NET的Delegate对象同样可以分解成两个部分:委托的功能(Method)和目标对象(Target),这可以直接从Delegate的定义就可以看出来:

       1:  
       2: public abstract class Delegate : ICloneable, ISerializable
       3: {
       4:     // Others
       5:     public MethodInfo Method { get; }
       6:     public object Target { get; }
       7: }

    我们最常用的两个事件处理类型EventHandlerEventHandler<TEventArgs>本质上就是一个Delegate,下面是它们的定义。但是并不是直接继承自System.Delegate,而是继承自System.MulticastDelegateMulticastDelegate派生于DelegateMulticastDelegate不涉及到本篇文章的主题,在这里就不再赘言介绍了。

       1: [Serializable, ComVisible(true)]
       2: public delegate void EventHandler(object sender, EventArgs e);
       3:  
       4: [Serializable]
       5: public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs: EventArgs;

    回到我们上篇关于TodoList的例子,在TodoListForm加载的时候,注册了单例对象TodolistManager的TodoListChanged事件,相关的代码如下

       1: private void TodoListForm_Load(object sender, EventArgs e)
       2: {
       3:     SynchronizationContext = SynchronizationContext.Current;
       4:     TodoListManager.Instance.TodoListChanged += TodoListManager_TodoListChanged;    
       5: } 
       6:  
       7: private void TodoListManager_TodoListChanged(object sender, TodoListEventArgs e)
       8: {
       9:     SynchronizationContext.Post(
      10:         state =>
      11:         {
      12:             BindingSource bindingSource = new BindingSource();
      13:             bindingSource.DataSource = e.TodoList;
      14:             this.dataGridViewTodoList.DataSource = bindingSource;
      15:         }, null);
      16: }

    image 经过简单一句事件注册代码就通过一个EventHandler(在本例中具体类型为EventHandler<TodoListArgs>)事件的源(Source,即TodoListManager)和事件的监听者(即TodoListForm)两着关联起来,三者之间的关系如右图(点击图片看大图)所示。从这张图中我们可以看到:TodoListForm实际上是通过注册的EventHandler的Target属性被TodoListManager间接引用着的。所以才会导致TodoListForm在关闭之后,永远不能拿成为垃圾对象,因为TodoListManager是一个基于static属性定义的Singleton对象,永远是GC的根。

    三、有什么方式能够更好的解决这个问题吗?

    上面的这个问题可以简单地通过在某些时机解除事件的注册的方式来解决,所以很多人认为这是由不好的编程习惯造成的,不应该是一个问题。不错,作为一个优秀的编程人员,在编写事件注册的时候应该具一种意识:是否应该在某个时机解除该事件的注册。但是,再强的老虎也有打盹的时候,况且我们面对的开发人员也许没有你想的那么优秀。此外,作为一个架构师或者是框架的设计者,是否应该考虑提高你应用的容错能力呢?我的意思是:既然这是一个大家普遍会犯的毛病,那么你应该考虑提高你程序的健壮性以容忍开发人员犯这种“大众性的错误”。

    image 如何来更好地解决这个问题呢?实际上我们的目的很单纯:当对象A注册到B的某个事件上,A并不受到B的“强制引用”。我想说道这里,有些读者应该心理有了答案:既然不能“强引用(Strong Reference)”,那就只能是“弱引用(Weak Reference)”。不错,我们就是通过System.WeakReference来解决这个问题。具体来讲,我们需要采取某种机制,让事件源(Event Source)的EventHandler通过WeakReference的方式与事件监听者建立关系。只有在这种情况下,事件监听者没有了事件源的强制引用,在我们不用的时候才能及时成为垃圾对象,等待GC对它的清理。右图(点击图片看大图)很好的揭示了这种解决方案的本质。

    我们具体的做法其实并不复杂,仅仅是写了如下一个特殊的WeakReferenceHandler对现有的EventHandler<TEventArgs>进行了改造,下面是实现WeakReferenceHandler的所有代码。我们通过传入EventHandler<TEventArgs>对象构造WeakReferenceHandler,在EventHandler<TEventArgs>的Target属性基础上建立WeakReference对象,在执行处理事件的时候通过该WeakReference找到真正的目标对象,如果找得到则通过反射在其基础上调用相应的方法;反之,如果通过不能得到Target,那么表明该事件的监听对象已经被GC当作垃圾对象回收掉了。为了在注册事件的时候方遍,特定义了一个隐式的类型转换:WeakReferenceHandler转换成EventHandler<TEventArgs>

       1: using System;
       2: using System.Reflection;
       3: namespace Artech.MemLeakByEvents
       4: {
       5:     public class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
       6:     {
       7:         public WeakReference Reference
       8:         { get; private set; }
       9:  
      10:         public MethodInfo Method
      11:         { get; private set; }
      12:  
      13:         public EventHandler<TEventArgs> Handler
      14:          { get; private set; }
      15:  
      16:         public WeakEventHandler(EventHandler<TEventArgs> eventHandler)
      17:         {
      18:             Reference = new WeakReference(eventHandler.Target);
      19:             Method = eventHandler.Method;
      20:             Handler = Invoke;
      21:         }
      22:  
      23:         public void Invoke(object sender, TEventArgs e)
      24:         {
      25:             object target = Reference.Target;
      26:             if (null != target)
      27:             {
      28:                 Method.Invoke(target, new object[] { sender, e });
      29:             }
      30:         }
      31:  
      32:         public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakHandler)
      33:         {
      34:             return weakHandler.Handler;
      35:         }
      36:     }
      37: }

    那么在实际进行事件注册的时候,你就可以采用下面的方式了,照样很简单,对不对?

       1: private void TodoListForm_Load(object sender, EventArgs e)
       2: {   
       3:     TodoListManager.Instance.TodoListChanged += new WeakEventHandler<TodoListEventArgs>(TodoListManager_TodoListChanged);
       4: }

    不过,任何事情都有其两面性,很难同时兼顾(比如软件架构下的Performance和Scalability),基于上面这种解决方式虽然能够有效地解决由于事件注册导致的内存泄露问题,但是会带来一定的性能损失,毕竟原来直接的事件注册方式是一种“强类型的Delegate”,具有更好的执行性能。关于本篇文章提供的实现方式,基本上借鉴了这篇文章:《[转]如何解决事件导致的Memory Leak问题:Weak Event Handlers》,有兴趣的朋友不妨认真读读。

    作者:Artech
    出处:http://artech.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Nginx性能优化CPU篇
    Goroutine
    GO多路复用
    GO并发安全
    16.同一把锁
    bzoj2600 [Ioi2011]ricehub 双指针
    CF1103C Johnny Solving (Codeforces Round #534 (Div. 1)) 思维+构造
    bzoj4764 弹飞大爷 LCT
    NOIP2019(CSP2019) 游记
    bzoj4530 [Bjoi2014]大融合 子树信息 LCT
  • 原文地址:https://www.cnblogs.com/artech/p/1618239.html
Copyright © 2020-2023  润新知