程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Release編譯模式下,事件是否會引起內存洩漏問題初步研究

Release編譯模式下,事件是否會引起內存洩漏問題初步研究

編輯:關於.NET

記:不常發生的事件內存洩漏現象

想必有些朋友可能也常常使用事件,但是很少解除事件掛鉤程序也沒有聽說過內存洩漏之類的問題。幸運的是,在某些情況下,的確不會出問題,很多年前做的項目就跑得好好的,我們靜態可以做一個實驗來再次驗證下。為了驗證這個問題,我一度懷疑自己代碼寫錯了,甚至照著書上(網上)例子寫也無法重現事件引起內存洩漏的問題,難道教科書說錯了麼?

首先來看看我的代碼,先准備2個類,一個發起事件,一個處理事件:

 class A
    {
        public event EventHandler ToDoSomething ;
        public A()
        {
        }

        public void RaiseEvent()
        {
            ToDoSomething(this, new EventArgs());
        }

        public void DelEvent()
        {
            ToDoSomething = null;
        }

        public void Print(string msg)
        {
            Console.WriteLine("A:{0}", msg);
           
        }
    }
    class B
    {
        byte[] data = null;
       
        public B(int size)
        {
            data = new byte[size];
            for (int i = 0; i < size ; i++)
                data[i] = 0;
        }

        public  void PrintA(object sender, EventArgs e)
        {
            ((A)sender).Print("sender:"+ sender.GetType ());
        }
    }

然後,在主程序裡面寫下面的方法:

 static void TestInitEvent(A a)
        {
            var b = new B(100 * 1024 * 1024);
            a.ToDoSomething += b.PrintA;
        }

 這裡將初始化一個 100M的對象B的實例b,然後讓對象a的事件掛鉤在b的方法PrintA 上。平常情況下,b是方法內部的局部變量,但由於b對象的方法掛鉤在了方法參數 a 對象的事件上,所以在這裡對象 b的生命周期沒有結束,這可以稍後由對象 a發起事件,b的 PrintA 方法被調用得到證實。

為了監測當前測試耗費了多少內存,准備一個方法  getWorkingSet,代碼如下:

 static void getWorkingSet() 
        {
            using (var process = Process.GetCurrentProcess()) 
            {
                Console.WriteLine("---------當前進程名稱:{0}-----------",process.ProcessName);
                using (var p1 = new PerformanceCounter("Process", "Working Set - Private", process.ProcessName))
                using (var p2 = new PerformanceCounter("Process", "Working Set", process.ProcessName))
                {
                    Console.WriteLine(process.Id);
                    //注意除以CPU數量
                    Console.WriteLine("{0}{1:N} KB", "工作集(進程類)", process.WorkingSet64 / 1024);
                    Console.WriteLine("{0}{1:N} KB", "工作集 ", process.WorkingSet64 / 1024);
                    // process.PrivateMemorySize64 私有工作集 不是很准確,大概多9M 
                    Console.WriteLine("{0}{1:N} KB", "私有工作集 ", p1.NextValue() / 1024); //p1.NextValue()
                    //Logger("{0};內存(專用工作集){1:N};PID:{2};程序名:{3}", 
                    //             DateTime.Now, p1.NextValue() / 1024, process.Id.ToString(), process.ProcessName);
                   
                }
            }
            Console.WriteLine("--------------------------------------------------------");
            Console.WriteLine();
           
        }

 

下面,開始在主程序裡面開始寫如下測試代碼:

           getWorkingSet();
            A a = new A();
            TestInitEvent(a);
            Console.WriteLine("1,按下任意鍵開始垃圾回收");
            Console.ReadKey();
            GC.Collect();
            getWorkingSet();

看屏幕輸出:

---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)25,260.00 KB
工作集 25,260.00 KB
私有工作集 8,612.00 KB
--------------------------------------------------------

1,按下任意鍵開始垃圾回收
---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)135,236.00 KB
工作集 135,236.00 KB
私有工作集 111,256.00 KB

程序開始運行後,正好多了100M內存占用。當前程序處於IDE的調試狀態下,然後,我們直接運行測試程序,不調試(Release),再次看下結果:

---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)10,344.00 KB
工作集 10,344.00 KB
私有工作集 7,036.00 KB
--------------------------------------------------------

1,按下任意鍵開始垃圾回收
---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)121,460.00 KB
工作集 121,460.00 KB
私有工作集 109,668.00 KB
--------------------------------------------------------

可以看到在Release 編譯模式下,內存還是沒法回收。

分析下上面這段測試程序,我們只是在一個單獨的方法內掛鉤了一個事件,並且事件還沒有執行,緊接著開始垃圾回收,但結果顯示沒有成功。這個符合我們教科書上說的情況:對象的事件掛鉤之後,如果不解除掛鉤,可能造成內存洩漏。

同時,上面的結果也說明了被掛鉤的對象 b 有沒有被回收,那麼發起事件來測試下,看b對象是否還能夠繼續處理別人發起的事件,繼續上面主程序代碼:

 Console.WriteLine("2,按下任意鍵,主對象發起事件");
            Console.ReadKey();
            a.RaiseEvent();//此處內存不能正常回收
            getWorkingSet();

結果:

2,按下任意鍵,主對象發起事件
A:sender:ConsoleApplication1.A
---------當前進程名稱:ConsoleApplication1-----------
7056
工作集(進程類)121,576.00 KB
工作集 121,576.00 KB
私有工作集 109,672.00 KB
--------------------------------------------------------

 這說明,雖然對象 b 脫離了方法 TestInitEvent 的范圍,但它依然存活,打印了一句話:A:sender:ConsoleApplication1.A

是不是GC多回收幾次才能夠成功呢?

我們繼續在主程序上調用GC試試看:

  Console.WriteLine("3,按下任意鍵開始垃圾回收,之後再次發起事件");
            Console.ReadKey();
            GC.Collect();
            a.RaiseEvent();//此處內存不能正常回收
            getWorkingSet();

結果:

3,按下任意鍵開始垃圾回收,之後再次發起事件
A:sender:ConsoleApplication1.A
---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)14,424.00 KB
工作集 14,424.00 KB
私有工作集 2,972.00 KB
--------------------------------------------------------

果然,內存被回收了,但請注意,我們在GC執行成功後,仍然調用了發起事件的方法  a.RaiseEvent();並且得到了成功執行,這說明,對象b 仍然存活,不過它內部大量無用的內存被回收了。

注意:上面這段代碼的結果是我再寫博客過程中,一邊寫一遍測試偶然發現的情況,如果是連續執行的,情況並不是這樣,上面這端代碼不能回收成功內存。
這說明,GC內存回收的時機,的確是不確定的。

繼續,我們注銷事件,解除事件掛鉤,再看結果:

 Console.WriteLine("4,按下任意鍵開始注銷事件,之後再次垃圾回收");
            Console.ReadKey();
            a.DelEvent();
            GC.Collect();
            Console.WriteLine("5,垃圾回收完成");
            getWorkingSet();

結果:

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)15,252.00 KB
工作集 15,252.00 KB
私有工作集 3,196.00 KB
--------------------------------------------------------

內存沒有明顯變化,說明之前的內存的確成功回收了。

 

為了印證前面的猜測,我們讓程序重新運行並且連續執行(Release模式),來看看執行結果:

---------當前進程名稱:ConsoleApplication1-----------
4280
工作集(進程類)10,364.00 KB
工作集 10,364.00 KB
私有工作集 7,040.00 KB
--------------------------------------------------------

1,按下任意鍵開始垃圾回收
---------當前進程名稱:ConsoleApplication1-----------
4280
工作集(進程類)121,456.00 KB
工作集 121,456.00 KB
私有工作集 109,668.00 KB
--------------------------------------------------------

2,按下任意鍵,主對象發起事件
A:sender:ConsoleApplication1.A
---------當前進程名稱:ConsoleApplication1-----------
4280
工作集(進程類)121,572.00 KB
工作集 121,572.00 KB
私有工作集 109,672.00 KB
--------------------------------------------------------

3,按下任意鍵開始垃圾回收,之後再次發起事件
A:sender:ConsoleApplication1.A
---------當前進程名稱:ConsoleApplication1-----------
4280
工作集(進程類)121,628.00 KB
工作集 121,628.00 KB
私有工作集 109,672.00 KB
--------------------------------------------------------

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1-----------
4280
工作集(進程類)19,228.00 KB
工作集 19,228.00 KB
私有工作集 7,272.00 KB
--------------------------------------------------------
View Code

這次的確印證了前面的說明,GC真正回收內存的時機是不確定的。

 

編譯器的優化

精簡下之前的測試代碼,僅初始化事件對象然後就GC回收,看看結果:

getWorkingSet();
            A a = new A();
            TestInitEvent(a);
 getWorkingSet();

            Console.WriteLine("4,按下任意鍵開始注銷事件,之後再次垃圾回收");
            Console.ReadKey();
            a.DelEvent();
            GC.Collect();
            Console.WriteLine("5,垃圾回收完成");
            getWorkingSet();
            Console.ReadKey();

 結果:

---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)10,344.00 KB
工作集 10,344.00 KB
私有工作集 7,240.00 KB
--------------------------------------------------------

---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)121,500.00 KB
工作集 121,500.00 KB
私有工作集 110,292.00 KB
--------------------------------------------------------

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)19,788.00 KB
工作集 19,788.00 KB
私有工作集 7,900.00 KB
--------------------------------------------------------

符合預期,GC之後內存恢復到正常水平。

將上面的代碼稍加修改,僅僅注釋掉GC前面的一句代碼:a.DelEvent();

getWorkingSet();
            A a = new A();
            TestInitEvent(a);
 getWorkingSet();

            Console.WriteLine("4,按下任意鍵開始注銷事件,之後再次垃圾回收");
            Console.ReadKey();
            //a.DelEvent();
            GC.Collect();
            Console.WriteLine("5,垃圾回收完成");
            getWorkingSet();
            Console.ReadKey();

再看結果:

---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)10,308.00 KB
工作集 10,308.00 KB
私有工作集 7,040.00 KB
--------------------------------------------------------

---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)121,256.00 KB
工作集 121,256.00 KB
私有工作集 7,592.00 KB
--------------------------------------------------------

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1-----------

工作集(進程類)19,436.00 KB
工作集 19,436.00 KB
私有工作集 7,600.00 KB
--------------------------------------------------------

大跌眼鏡:居然沒有發生大量內存占用的情況!

看來只有一個可能性:

對象a 在GC回收內存之前,沒有操作事件之類的代碼,因此可以非常明確對象a 之前的事件代碼不再有效,相關的對象b可以在  TestInitEvent(a); 方法調用之後立刻回收,這樣就看到了現在的測試結果。

如果不是 Release 編譯模式優化,我們來看看在IDE調試或者Debug編譯模式運行的結果(前面的代碼不做任何修改):

---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)25,148.00 KB
工作集 25,148.00 KB
私有工作集 9,816.00 KB
--------------------------------------------------------

---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)136,048.00 KB
工作集 136,048.00 KB
私有工作集 112,888.00 KB
--------------------------------------------------------

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)136,692.00 KB
工作集 136,692.00 KB
私有工作集 112,892.00 KB
--------------------------------------------------------


這一次,盡管仍然調用了GC垃圾回收,但實際上根本沒有立刻起到效果,內存仍然100多M。

 

最後,我們在發起事件掛鉤之後,立即解除事件掛鉤,再看下Debug模式下的結果,為此僅僅需要修改下面代碼一個地方:

     static void TestInitEvent(A a)
        {
            var b = new B(100 * 1024 * 1024);
            a.ToDoSomething += b.PrintA;
            //
            a.ToDoSomething -= b.PrintA;
        }

然後看在Debug模式下的執行結果:

---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)26,344.00 KB
工作集 26,344.00 KB
私有工作集 9,452.00 KB
--------------------------------------------------------

---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)135,628.00 KB
工作集 135,628.00 KB
私有工作集 10,008.00 KB
--------------------------------------------------------

4,按下任意鍵開始注銷事件,之後再次垃圾回收
5,垃圾回收完成
---------當前進程名稱:ConsoleApplication1.vshost-----------

工作集(進程類)33,768.00 KB
工作集 33,768.00 KB
私有工作集 10,008.00 KB
--------------------------------------------------------

符合預期,內存占用量沒有增加,所以此時調用GC回收內存都沒有意義了。

 

總結

使用事件的時候如果不在使用完之後解除事件掛鉤,的確可能發生內存洩漏,

並且GC內存回收的時機的確具有不確定性,所以GC不是救命稻草,最佳的做法還是用完事件立即解除事件掛鉤。

如果你忘記了這個事情,也請一定不要忘記發布程序的時候,使用Release編譯模式!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved