系列主题:基于消息的软件架构模型演变

在Winform和Asp.net时代,事件被大量的应用在UI和后台交互的代码中。看下面的代码:

        private void BindEvent()
{
var btn = new Button();
btn.Click += btn_Click;
} void btn_Click(object sender, EventArgs e)
{
MessageBox.Show("click");
}

这样的用法可以引起内存泄漏吗?为什么我们平时一直写这样的代码从来没关注过内存泄漏?等分析完原因后再来回答这个问题。

为了测试原因,我们先写一个EventPublisher类用来发布事件:

    public class EventPublisher
{
public static int Count; public event EventHandler<PublisherEventArgs> OnSomething; public EventPublisher()
{
Interlocked.Increment(ref Count);
} public void TriggerSomething()
{
RaiseOnSomething(new PublisherEventArgs(Count));
} protected void RaiseOnSomething(PublisherEventArgs e)
{
EventHandler<PublisherEventArgs> handler = OnSomething;
if (handler != null) handler(this, e);
} ~EventPublisher()
{
Interlocked.Decrement(ref Count);
}
}

这个类提供了一个事件OnSomething,另外在构造函数和析构函数中分别会对变量Count进行累加和递减。Count的数量反应了EventPublisher的实例在内存中的数量。

写一个Subscriber用来订阅这个事件:

    public class Subscriber
{
public string Text { get; set; }
public List<StringBuilder> List = new List<StringBuilder>();
public static int Count;
public Subscriber()
{
Interlocked.Increment(ref Count);
for (int i = 0; i < 1000; i++)
{
List.Add(new StringBuilder(1024));
}
} public void ShowMessage(object sender, PublisherEventArgs e)
{
Text = string.Format("There are {0} publisher in memory",e.PublisherReferenceCount);
} ~Subscriber()
{
Interlocked.Decrement(ref Count);
}
}

Subscriber同样用Count来反映内存中的实例数量,另外我们在构造函数中使用StringBuilder开辟1000*1024Size的大小以方便我们观察内存使用量。

最后一步,写一个简单的winform程序,然后在一个Button的Click事件中写入测试代码:


         private void btnStartShortTimePublisherTest_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100; i++)
{
var publisher = new EventPublisher();
publisher.OnSomething += new Subscriber().ShowMessage;
publisher.TriggerSomething();
} MessageBox.Show(string.Format("There are {0} publishers in memory, {1} subscribers in memory", EventPublisher.Count, Subscriber.Count));
}

for循环中的代码是一个很普通的事件调用代码,我们将Subscriber实例中的ShowMessage方法绑定到了publisher对象的OnSomething事件上,为了观察内存的变化我们循环100次。

执行结果如下:

publisher和subscriber的数量都为3,这并不代表发生了内存泄漏,只不过是没有完全回收完毕而已。每个publisher在出了for循环后就会被认为没有任何用处,从而被正确回收。而注册在上面的观察者subscriber也能被正确回收。

再放一个Button,并在Click中写以下测试代码:

        private void BtnStartLongTimePublisher_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100; i++)
{
var publisher = new EventPublisher();
publisher.OnSomething += new Subscriber().ShowMessage;
publisher.TriggerSomething();
LongLivedEventPublishers.Add(publisher);
}
MessageBox.Show(string.Format("There are {0} publishers in memory, {1} subscribers in memory", EventPublisher.Count,Subscriber.Count));
}

这次for循环中不同之处在于我们将publisher保存在了一个list容器当中,从而保证100个publisher不能垃圾回收。这次的执行结果如下:

我们看到100个subscribers全部保存在内存中。如果观察资源管理器中的内存使用率,你也能发现内存突然涨了几百兆并且再不会减少。

想一下下面的场景:

public class Runner
{
private LongTimeService _service; public Runner()
{
_service = new LongTimeService(); }
public void Run()
{
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
_service.SomeThingUpdated += (o, e) => { /*do some thing*/};
}
}

LongTimeService是一个长期运行的服务,从来不被销毁,这将导致所有注册在SomeThingUpdated 事件上的观察者也不会能回收。当有大量的观察者不停的注册在SomeThingUpdated 上时,就会发生内存泄漏。

这三个测试说明了引起事件内存泄漏的场景:当观察者注册在了一个生命周期长于自己的事件主题上,观察者不能被内存回收。

解决办法是在事件上显示调用-=符号。

再回过头来看开始提出来的问题:当使用了Button的Click事件的时候,会发生内存泄漏吗?

btn.Click += btn_Click;

观察者是谁?btn_Click方法的拥有者,也就是Form实例。

主题是谁?Button的实例btn

主题btn什么时候销毁?当Form实例被销毁的时候。

当Form被销毁的时候,btn及其观察者都会被销毁。除非Form从来不销毁,并且大量的观察者持续注册在了btn.Click上才能发生内存泄漏,当然这种场景是很少见的。所以我们开发winform或者asp.net的时候一般来说并不会关心内存泄漏的问题。

.net中事件引起的内存泄漏分析的更多相关文章

  1. Javascript的内存泄漏分析

    作为程序员(更高大尚的称谓:研软件研发)的我们,无论是用Javascript,还是.net, java语言,肯定都遇到过内存泄漏的问题.只不过他们都有GC机制来帮助程序员完成内存回收的事情,如果你是C ...

  2. 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native ...

  3. Java内存泄漏分析与解决方案

    Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历 ...

  4. Android内存泄漏分析及调试

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先 ...

  5. Android 内存泄漏分析与解决方法

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...

  6. Java内存泄漏分析系列之五:常见的Thread Dump日志案例分析

    原文地址:http://www.javatang.com 症状及解决方案 下面列出几种常见的症状即对应的解决方案: CPU占用率很高,响应很慢 按照<Java内存泄漏分析系列之一:使用jstac ...

  7. Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析

    原文地址:http://www.javatang.com 一个典型的thread dump文件主要由一下几个部分组成: 上图将JVM上的线程堆栈信息和线程信息做了详细的拆解. 第一部分:Full th ...

  8. Android内存泄漏分析实战

    内存泄漏简单介绍 java能够保证当没有引用指向对象的时候,对象会被垃圾回收器回收.与c语言自己申请的内存自己释放相比,java程序猿轻松了非常多.可是并不代表java程序猿不用操心内存泄漏.当jav ...

  9. (转)Android内存泄漏分析及调试

      http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析  首先了解一下dalvik的Ga ...

随机推荐

  1. JS--该死的&&和||

    近段时间搞前端js,零零星星的看了一些框架源码,发现大量存在&&和||,其语法看的我是头冒青烟,也不知道怎么回事,度娘搜吧,现在写下来,以防忘掉. 先来约束一下用词:有一表达式A : ...

  2. css sprite,css雪碧图生成工具V3.0更新

    V3.0主要改进 1.增加了单独添加单张图片以及删除单张图片的功能 2.增加了生成.sprite文件用以保存雪碧图信息 3.增加了打开.sprite文件功能 什么是css sprite CSS spr ...

  3. [C#].NET中几种Timer的使用

    这篇博客将梳理一下.NET中4个Timer类,及其用法. 1. System.Threading.Timer public Timer(TimerCallback callback, object s ...

  4. LinkedList源码阅读笔记(基于JDK1.8)

    LinkedList是List接口的一个有序链表实现,存储节点是内部类Node,Node中有两个属性prev和next,负责连接前后两个元素.由于不是使用数组进行存储,所以查询需要遍历链表一半的元素( ...

  5. 安装sqoop

    安装sqoop 1.默认已经安装好java+hadoop 2.下载对应hadoop版本的sqoop版本 3.解压安装包 tar zxvf sqoop-1.4.6.bin__hadoop-2.0.4-a ...

  6. js/jQuery使用过程中常见问题

    目录 一.jQuery选择器选择选中的或者disabled的选择框时attr函数无效 二.jQuery each函数的break/continue 三.jQuery 获取元素的left会值/left数 ...

  7. 一、PID控制原理

    在模拟控制系统中,控制器最常用的控制规律是PID控制.模拟PID控制系统原理框图如下图.系统由模拟PID控制器和被控对象组成. PID控制器是一种线性控制器,它根据给定值Yd(t)与实际输出值Y(t) ...

  8. 测试...外部指针访问private

    #include<iostream> using namespace std; class A{ public: int* getPointer(){ return &m; } v ...

  9. 弱省互测#0 t3

    Case 1 题意 要求给出下面代码的答案然后构造输入. 给一个图, n 个点 m 条边 q 次询问,输出所有点对之间最大权值最小的路径. 题解 把每一个询问的输出看成一条边,建一棵最小生成树. Ca ...

  10. CentOS 7 防止端口自动关闭

    tl;dr firewall-cmd --permanent --zone=public --add-port=2888/tcp firewall-cmd --reload #重新载入服务 永久配置f ...