事件(Event),绝大多数内存泄漏(Memory Leak)的元凶[上篇]

最近这两天一直在忙着为一个项目检查内存泄漏(Memory Leak)的问题,对相关的知识进行了一下简单的学习和探索,其间也有了一些粗浅的经验积累,今天特意写一篇相关的文章与大家分享。那些对内存泄漏稍微有点了解的人,对于本篇文章的标题,相信不会觉得是在危言耸听。就我查阅的资料,已经这两天的发现也证实了这一点:觉得部分的内存泄漏问题与事件(Event)有关。本篇文章将会介绍其原理,以及如何发现和解决由事件导致的内存泄漏问题。

为了让读者首先对这个主题有一个感官的印象,让大家觉得内存泄漏问题离我们并不遥远,我特意写了一个简单的应用程序。我们这个应用程序叫做TodoListManager,因为通过它可以实时查看属于用户的“待办事宜(Todolist)”。这是一个GUI的应用,有两个Windows Form组成:左侧的窗体是一个程序的主界面(为了简单起见,我甚至没有将其做成MDI窗体),点击Todo List菜单项,右面的Form被显示出来:所有的代码事宜将会全部列出,为了保证记录的实时显示,每隔5秒钟数据自动刷新一次。

首先定义表示每一项TotoList Item定义了一个相应的类型:Event(不是我们谈到的导致内存泄漏的事件)。Event仅仅包含简单的属性:主题(Subject),截至日期(DueDate)和相应的描述性文字(Description),Event定义如下:

   1: using System;
   2: namespace Artech.MemLeakByEvents
   3: {
   4:     public class Event
   5:     {
   6:         public string Subject { get; set; }
   7:         public DateTime DueDate { get; set; }
   8:         public string Description { get; set; }
   9:         public Event(string subject, DateTime dueDate, string desc)
  10:         {
  11:             if (string.IsNullOrEmpty(subject))
  12:             {
  13:                 throw new ArgumentNullException("subject");
  14:             }
  15:             this.Subject = subject;
  16:             this.DueDate = dueDate;
  17:             this.Description = desc ?? string.Empty;
  18:         }
  19:     }
  20: }

然后我将所有逻辑(实际上仅仅是定期获取TodoList列表而已)定义在下面一个叫做TodoListManager的类型中。为了演示的需要,我特意将其定义成Singleton的形式,并采用System.Threading.Timer实现定时地获取Todo List的操作。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Threading;
   4: namespace Artech.MemLeakByEvents
   5: {
   6:     public  class TodoListManager
   7:     {
   8:         private static readonly TodoListManager instance = new TodoListManager();
   9:         public event EventHandler<TodoListEventArgs> TodoListChanged;
  10:         private Timer todoListRefreshSchedler;
  11:         private TodoListManager()
  12:         {
  13:             todoListRefreshSchedler = new Timer
  14:             (
  15:                 state =>
  16:                 {
  17:                     if (null == TodoListChanged)
  18:                     {
  19:                         return;
  20:                     }
  21:  
  22:                     TodoListChanged(null, new TodoListEventArgs(GetTodolist()));
  23:                 }
  24:            , null, 0, 5000);
  25:         }
  26:         public static TodoListManager Instance
  27:         {
  28:             get
  29:             {
  30:                 return instance;
  31:             }
  32:         }
  33:         private List<Event> GetTodolist()
  34:         {
  35:             var list = new List<Event>();
  36:             list.Add(new Event("Meeting with Testing Team", DateTime.Today.AddDays(2),"NIL"));
  37:             list.Add(new Event("Deliver progress report to manager ", DateTime.Today.AddDays(7), "NIL"));
  38:             return list;
  39:         }
  40:     }
  41: }

对于Timer的每一个轮询,都会处触发一个类型为EventHandler<TodoListEventArgs>的事件,通过注册这个事件,可以通过类型为TodoListEventArgs的事件参数得到最新的TodoList的列表,TodoListEventArgs定义如下:

   1: using System;
   2: using System.Collections.Generic;
   3: namespace Artech.MemLeakByEvents
   4: {
   5:     public class TodoListEventArgs : EventArgs
   6:     {
   7:         public IEnumerable<Event> TodoList
   8:         { get; private set; }
   9:         public TodoListEventArgs(IEnumerable<Event> todoList)
  10:         {
  11:             if (null == todoList)
  12:             {
  13:                 throw new ArgumentNullException("todoList");
  14:             }
  15:  
  16:             this.TodoList = todoList;
  17:         }
  18:     }
  19: }

然后我们来看看我们的应用通过怎样的形式将每一个刷新的列表显示在TodolList窗体中。其实很简单,我仅仅是在窗体Load的时候注册TodoListManager的TodoListChanged事件,并将获取到的TodoList列表绑定到DataGridView上面。由于TodoListManager异步工作的原因,我借助了SynchronizationContext这么一个对象实现对数据的绑定。

   1: using System;
   2: using System.Threading;
   3: using System.Windows.Forms;
   4:  
   5: namespace Artech.MemLeakByEvents
   6: {
   7:     public partial class TodoListForm : Form
   8:     {
   9:         public static SynchronizationContext SynchronizationContext
  10:         { get; private set; }
  11:  
  12:         public TodoListForm()
  13:         {
  14:             InitializeComponent();
  15:         }
  16:  
  17:         private void TodoListForm_Load(object sender, EventArgs e)
  18:         {
  19:             SynchronizationContext = SynchronizationContext.Current;
  20:             TodoListManager.Instance.TodoListChanged += TodoListManager_TodoListChanged;            
  21:         }
  22:  
  23:         private void TodoListManager_TodoListChanged(object sender, TodoListEventArgs e)
  24:         {
  25:             SynchronizationContext.Post(
  26:                 state =>
  27:                 {
  28:                     BindingSource bindingSource = new BindingSource();
  29:                     bindingSource.DataSource = e.TodoList;
  30:                     this.dataGridViewTodoList.DataSource = bindingSource;
  31:                 }, null);
  32:         }       
  33:     }
  34: }

整个应用就这么简单,但是为了确定是否真的出现内存泄漏,我们需要在查看内存状态的时候,确保GC把所有垃圾对象全部回收完毕。为此,我在整个应用级别定义了一个静态的System.Threading.Timer,让它每隔半秒调用一次GC.Collect()。

   1: using System;
   2: using System.Windows.Forms;
   3: namespace Artech.MemLeakByEvents
   4: {
   5:     static class Program
   6:     {
   7:         static System.Threading.Timer gcScheduler = new System.Threading.Timer
   8:             (state => GC.Collect(), null, 0, 500);       
   9:         [STAThread]
  10:         static void Main()
  11:         {
  12:             Application.EnableVisualStyles();
  13:             Application.SetCompatibleTextRenderingDefault(false);
  14:             Application.Run(new MainForm());
  15:         }
  16:     }
  17: }

接下来我查看我们的应用程序是否会有内存泄漏的问题了。查看内存泄漏,当然不能通过我们的肉眼去捕捉,需要借助响应的Memory Profiling工具。我们有很多这样的工具,有免费的,也有需要付钱购买的。在这里我推荐两个Memory Profiling工具,一个是JetBrains的dotTrace,另一个是RedGate的ANTS Memory Profiler,前者是免费的,后者不是。在这里我通过后者来查看本应用的内存泄漏问题。

ANTS Memory Profiler通过这样的原理来确定你的应用程序是否有泄漏问题:如果你怀疑某个操作会导致应该被GC回收的对象没有被回收,那么你在之前对内存分配情况拍一张快照(Snapshot),然后执行该操作,在操作完成并确定GC完成相应的回收操作后,在拍一张快照。通过对比,找出多余的对象,并根据具体的情况分析该对象是否应该被GC回收,如果是的,怎意味着你的程序存在着内存泄漏问题。关于ANTS Memory Profiler的具体操作,这里就不再细说了,只要大家了解基本的原理,不影响对后面内容的理解就可以了。

通过ANTS Memory Profiler启动我们的应用程序后,在一开始的时候我们拍摄一张反映程序初始状态的内存快照,然后选择File\Todo List打开TodoListForm,等待一定的时间,再将TodoListForm关闭。为了让GC有充分的时间进行垃圾回收,不妨再作相应的等待,然后拍下第二张快照。在Class List视图中,你会发现原本应该被垃圾回收的TodoListForm窗体对象还存在于内存之中。

那么是什么导致TodoListForm不能GC正常回收呢?熟悉GC原理的人应该知道,原因只有一个,那就是被某些正在使用或者会被使用,或者GC认为正在正在使用或者会被使用的对象引用着(Jeffrey Richiter将这些对象成为所谓的根)。ANTS Memory Profiler的强大之处就是可以让你可以很清楚地看到这个对象正在被那些其他的对象引用着。

左图就是TodoListForm对象在内存中的引用链,我们可以很清楚地看到:该对象被TodoListManager的一个类型为EventHandler<TodoListEventArgs>的事件引用,这个对象实际上是一个Delegate对象,而TodoListForm作为这个Delegate对象的Target。通过上面给出的代码,我们不难想出是由于在TodoListForm实现了对TodoListManager的TotoListChanged事件注册导致了TodoListManager不能被垃圾回收。

上面的实力说明了这么一种情况:对于GUI应用可视化树形结构来说,一个窗体被关闭,照例说它应该成为垃圾对象,GC在执行垃圾回收的时候就可以将其清楚的。但是,由于该对象注册了一个事件到一个生命周期很长的对象(在本例中,TodoManager是一个Singletone对象,具有和整个应用程序一样的生命周期),它就是被这么一个对象长期引用,进而阻止 GC对其的回收工作。

所以,在这种情况:短暂生命周期注册事件到长期生命周期对象上,在该对象被Dispose的时候,应该解除事件的注册。你可以通过实现System.IDisposable接口,将解除事件注册的操作放在Dispose方法中。对于本里来说,你可以将相应的操作注册到Form的Closing、Closed或者Disposed事件中。比如在下面代码中,我为TodoListForm添加了如下一个Closing事件处理程序:

   1: using System;
   2: using System.Threading;
   3: using System.Windows.Forms;
   4:  
   5: namespace Artech.MemLeakByEvents
   6: {
   7:     public partial class TodoListForm : Form
   8:     {
   9:         //省略其他成员
  10:         private void TodoListManager_TodoListChanged(object sender, TodoListEventArgs e)
  11:         {
  12:             SynchronizationContext.Post(
  13:                 state =>
  14:                 {
  15:                     BindingSource bindingSource = new BindingSource();
  16:                     bindingSource.DataSource = e.TodoList;
  17:                     this.dataGridViewTodoList.DataSource = bindingSource;
  18:                 }, null);
  19:         }
  20:  
  21:         private void TodoListForm_FormClosing(object sender, FormClosingEventArgs e)
  22:         {
  23:             TodoListManager.Instance.TodoListChanged -= TodoListManager_TodoListChanged;
  24:         }
  25:     }
  26: }

那么,在此按照上面的流程利用ANTS Memory Profiler查看内存泄漏,在第二个快照中,你将再也看不到TodoListForm的身影(如下图)。本篇主要介绍如何重现事件注册导致内存泄露,已及最直接的解决方案。下一篇我将进一步对其背后的原理进行剖析,并提出另一种更加“优雅而可靠”解决方案。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏柠檬先生

Angularjs基础(四)

AngularJS过滤器     过滤器可以使用一个管道符(|)添加到表达式和指令中。       AngularJS过滤器可用于转换数据:    ...

1989
来自专栏NetCore

ADO.NET 2.0 中的新增 DataSet 功能

ADO.NET 2.0 中的新增 DataSet 功能 发布日期: 1/13/2005 | 更新日期: 1/13/2005 Jackie Goldstein ...

18510
来自专栏walterlv - 吕毅的博客

.NET 命令行参数包含应用程序路径吗?

发布于 2018-09-11 13:28 更新于 2018-09...

893
来自专栏Golang语言社区

游戏服务器之内存数据库redis客户端应用(下)

(3)存储一个角色的基础信息(使用命令set) 存储结构: key:BASE角色id ,value 角色基础信息 int playerId = player-...

5398
来自专栏小灰灰

Java 动手写爬虫: 一、实现一个最简单爬虫

第一篇 准备写个爬虫, 可以怎么搞? 使用场景 先定义一个最简单的使用场景,给你一个url,把这个url中指定的内容爬下来,然后停止 一个待爬去的网址(有个地...

6166
来自专栏技术博客

ExtJs四(ExtJs MVC登录窗口的调试)

继上一节中实现了验证码http://www.cnblogs.com/aehyok/archive/2013/04/19/3030212.html,现在我们可以进...

1062
来自专栏码农阿宇

.Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow

在学习的过程中,看一些一线的技术文档很吃力,而且考虑到国内那些技术牛人英语都不差的,要向他们看齐,所以每天下班都在疯狂地背单词,博客有些日子没有更新了,见谅见谅...

991
来自专栏林德熙的博客

WPF 如何在绑定失败异常

在开发 WPF 程序,虽然 xaml 很好用,但是经常会出现小伙伴把绑定写错了。因为默认的 VisualStudio 是没有自动提示,这时很容易复制粘贴写出一个...

2211
来自专栏ASP.NET MVC5 后台权限管理系统

ASP.NET MVC5+EF6+EasyUI 后台管理系统(21)-权限管理系统-跑通整个系统

这一节我们来跑通整个系统,验证的流程,通过AOP切入方式,在访问方法之前,执行一个验证机制来判断是否有操作权限(如:增删改等) 原理:通过MVC自带筛选器,在筛...

5687
来自专栏大内老A

如何解决分布式系统中的跨时区问题[原理篇]

《谈谈你最熟悉的System.DateTime[上篇][下篇]》从跨时区的角度对DateTime这个我们熟知的类型进行了深入探讨,它们都是为这篇文章作的准备工作...

1907

扫码关注云+社区