zoukankan      html  css  js  c++  java
  • .net中事件引起的内存泄漏分析

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

    在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次。

    执行结果如下:

    Unnamed QQ Screenshot20151024202623

    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不能垃圾回收。这次的执行结果如下:

    Unnamed QQ Screenshot20151024202709

    我们看到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的时候一般来说并不会关心内存泄漏的问题。

  • 相关阅读:
    散点图增加趋势线
    asp.net自动刷新获取数据
    jQuery在asp.net中实现图片自动滚动
    tornado 学习笔记一:模板
    node.js 学习笔记二:创建属于自己的模块
    mysql 5.6 innodb memcache 操作学习一
    unicode,str
    node.js 学习笔记四:读取文件
    gevent 学习笔记一
    while 循环居然可以用else
  • 原文地址:https://www.cnblogs.com/richieyang/p/4907630.html
Copyright © 2011-2022 走看看