Debug/Release版運行結果異同--GC的引子

  有時咱們在運行程序時,會出現Debug版本和Release版本運行結果不一致的狀況。現給出C#中的一個例子,這個例子是Jeffrey在他的《CLR via C#》中"垃圾回收"這一章給出的,頗有意思。
  其實你們也會猜想,Debug版和Release版編譯後的代碼確定會有不同的地方,是的!是會這樣的。要否則也不會出現運行結果不一致,呵呵!閒話不說咱們來看看代碼,代碼很簡單。(調試環境WinXP + VS2003 + C#)
 

    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            TimerCallback tm = new TimerCallback(ShowTime);
            Timer t = new Timer(tm,null,0,2000);

            Console.ReadLine();
            //t.Dispose(); (1)
        }
        private static void ShowTime(Object o)
        {
            Console.WriteLine(DateTime.Now.ToString());
            GC.Collect();  //強制GC (2)
        }
    }
html

  
  你能夠在Debug版本中運行一下,會發現該報時程序工做正常(一直會報時)。而在Release版本中時間報只顯示一下。
  爲何呢?這是由於咱們在(2)處強制了一次GC(Garbage Collect)即垃圾回收。在Release版中當咱們程序運行到GC.Collect()時,Timer類型對象t是能夠被GC的。但Debug版本中t是做爲一個本地參數而不能被GC的。
  咱們再分解一下整個過程(僅列出關鍵過程):
    1,建立Timer類型對象t;
    2,t定時調用委託tm;
    3,tm方法執行;
    4,tm方法進行Garbage Collect;
    5,待讀取一行字符;(其間t定時器重複執行步驟2)
    6,退出;
  能夠看出第4,5步是個關鍵,若是咱們一直等待輸入就能不停打印。但問題是:在Debug版中確實是這樣的,在Release版中卻只打印了一次....How mysterious!

  TOM:    這時t是否是不工做了?
  JERRY:  對的,若是是Release版本中,至第5步時t確實不工做了,由於:它已經不存在了,被GC了.
  TOM:    不....不....不,你慢一點,別忽悠我^_^!!!,那Debug版爲何它就不被GC呢?
  JERRY:  這是因它,它在Debug版中不符合GC的條件.
  TOM:    你鼻子變長了....
  JERRY:  不信? 你看...這,這,這,諾..還有這.

   爲了讓TOM相信這是真的,JERRY用ILDasm分別打開了Debug版本和Release版本的程序並雙擊打開Main方法.
------------
Debug版:
-------------------------------------------------
  .........省略了一些行........
  // 代碼大小       32 (0x20)
  .maxstack  5
  .locals ([0] class [mscorlib]System.Threading.Timer t)
  IL_0000:  ldnull
  IL_0001:  ldftn      void Class1.Program::ShowTime(object)
  IL_0007:  newobj     instance void [mscorlib]
                       System.Threading.TimerCallback::.ctor(object,native int)
  IL_000c:  ldnull
  IL_000d:  ldc.i4.0
  IL_000e:  ldc.i4     0x7d0
  IL_0013:  newobj     instance void [mscorlib]System.Threading.Timer::.ctor
                       (class[mscorlibSystem.Threading.TimerCallback,
                       object,int32,int32)
  IL_0018:  stloc.0
  IL_0019:  call       string [mscorlib]System.Console::ReadLine()  
  IL_001e:  pop   
  IL_001f:  ret    //仍可以使用t對象
} // end of method Program::Main
 
-------------------------------------------------
  在Debug版中咱們定義的t是做爲一個本地參數(locals[0]),在 IL_0018位置, 它參考到新new的Timer對象.這時須要注意一點是的,即便在IL_001f處,仍經過locals[0]引用到t對象.
  因此在託管堆(Managed Heap)中,該對象仍象是可達對象(Reachable object).也就不符合被回收的條件。因此只要Main函數不結束,t就不能被回收。
 
---------------
Release版:
-------------------------------------------------
  .........省略了一些行........
  // 代碼大小       32 (0x20)
  .maxstack  5
  IL_0000:  ldnull
  IL_0001:  ldftn      void Class1.Program::ShowTime(object)
  IL_0007:  newobj     instance void [mscorlib]
                       System.Threading.TimerCallback::.ctor(object,native int)
  IL_000c:  ldnull
  IL_000d:  ldc.i4.0
  IL_000e:  ldc.i4     0x7d0
  IL_0013:  newobj     instance void [mscorlib]System.Threading.Timer::.ctor
                       (class[mscorlibSystem.Threading.TimerCallback,
                       object,int32,int32)
  IL_0018:  pop
  IL_0019:  call       string [mscorlib]System.Console::ReadLine()
  IL_001e:  pop
  IL_001f:  ret    //至此,也沒有辦法能夠refer to至t對象,t能夠被Garbage Collect
} // end of method Program::Main
-------------------------------------------------
  而在Rlease版中位置: IL_0018只用了pop方法彈出該對象(也就是t對象).這至關於丟棄了t對象,這時t被標記爲可回收。
  但有一點須要瞭解的是:t雖被丟棄,還仍存活在託管堆(Managed Heap)中,直到t調用ShowTime,而在ShowTime函數中調用GC爲止。當ShowTime中調用GC時,因爲t被標記爲可回收,因此t對象被回收。整個過程看起來是這個樣子的:
 
 
  正是因爲在Release版本中t是做爲臨時變量,用完後被GC強制回收,因此t只能工做一次,便經過調用GC結束了本身的生命,可憐的娃!
 
  爲了使Debug版和Release運行結果保持一致,你能夠有兩個選擇:
 
  1,在Console.ReadLine只後調用t.Dispose(),即取消我最早給出的代碼的(1)處的註釋。由於t對象在建立以後還要被引用,因此建立的t對象也被做爲一個本地參數來保存,生成的IL以下:
   .locals init (class [mscorlib]System.Threading.Timer V_0)
 
  2,註釋掉我最早給出的代碼的(2)處的GC.Collect函數的調用.這樣雖然在Release版中t對象已被標識爲可回收,但些時沒有內存需求(這只是一個假設),CLR並不會回收t對象.
  顯然方法2是不可取的,咱們只把但願寄託在CLR不進行Garbage Collect,但在現實編程中這是不現實的。在t被觸發的間隔間誰也不能保證CLR不進行Garbage Collect.若使用方法2我稍改了一下代碼,Release版本的程序又不能工做了。
 
 

    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
           Timer t = new Timer(new TimerCallback(ShowTime),null,0,500);
           System.Threading.Thread.Sleep(1000);  //新增
           Thread gc = new Thread(new ThreadStart(GcCollect));//新增
           gc.Start();  //新增

           Console.ReadLine();
        }
        private static void ShowTime(Object o)
        {
            Console.WriteLine(DateTime.Now.ToString());
        }
        private static void GcCollect()  //新增
        {
            GC.Collect();
        }
    }
編程

  
  這段代碼中,我雖沒有在ShowTime中進行GC,但另外一個線程的CoCollect函數卻調用了GC.Collect,這會迫使CLR進行Garbage Collect。t只是存活了一小會兒,仍舊被回收了。
  但若使用方法1,上面這段代碼在Release版本中仍能正常工做。
 
  後記,Garbage Colletion是一個彷佛很神祕的東西。由於時常咱們並不知道何時CLR進行Carbage Collect.之前經常將.net程序性能很差的緣由都"嫁禍"在GC的頭上,其實這是片面的。真正的緣由是咱們不瞭解GC,不瞭解GC的工做原理。
相關文章
相關標籤/搜索