.net垃圾回收機制編程調試試驗

1. 什麼是CLR GC?數據庫

它是一個基於引用跟蹤和代的垃圾回收器。緩存

從本質上,它爲系統中全部活躍對象都實現了一種引用跟蹤模式,若是一個對象沒有任何引用指向它,那麼這個對象就被認爲是垃圾對象,而且能夠被回收。GC經過代的概念來跟蹤對象的持續時間,活躍時間段的對象被歸爲0代,而活躍時間更長的被歸爲1代和2代。CLR認爲大多數對象都是短暫活躍的,所以0代被收集的頻率遠遠高於1代和2代。數據結構

看下GC中對象及其代齡分佈:性能

在.net中,初始分配的小對象在0代上; 經過垃圾回收後,存活的有根對象將被移動到後一代上。字體

有根對象(引用對象)有哪些?優化

1.靜態、全局對象,包括緩存和進程內Sessionui

2.Com對象計數器 this

3.線程堆棧上的局部變量釘釦對象,spa

4.本地API調用,Remoting/Webservice調用.net

5.finalizer 隊列裏的對象

 

先來一個簡單的實例程序,

 1 public class Student
 2     {
 3         public string Name { get; set; }
 4         public string Address { get; set; }
 5         public Student(string name, string address)
 6         {
 7             Name = name;
 8             Address = address;
 9         }
10     }
11     class Program
12     {
13         static void Main(string[] args)
14         {
15             Student wang = new Student("wang", "Beijing");
16             Student lee = new Student("lee", "Shanghai");
17 
18             //GC.Collect(); 
19             Console.ReadLine();
20         }
21     }

Run Windbg, 看下:

!eeheap -gc

看下每一代在CLR 堆中的起始地址,輸出關於GC的信息:

Number of GC Heaps: 1
generation 0 starts at 0x0000000002621030
generation 1 starts at 0x0000000002621018
generation 2 starts at 0x0000000002621000
ephemeral segment allocation context: none
         segment            begin         allocated             size
0000000002620000 0000000002621000  0000000002625fe8 0x0000000000004fe8(20456)
Large object heap starts at 0x0000000012621000
         segment            begin         allocated             size
0000000012620000 0000000012621000  0000000012627048 0x0000000000006048(24648)
Total Size            0xb030(45104)
------------------------------
GC Heap Size            0xb030(45104)

看紅色字體,得知:

「第2代的起始地址是 0x0000000002621000,第0代的起始地址是 0x0000000002621030」。 這裏暫時先記下,留着後面還會在看。

切換到主線程, 以便看當前程序堆棧,

~0s

關鍵時候到了,!clrstack -a,繼續看:

 

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x3f70 (0)
>...省略無關內容...
51 
52 00000000002ceef0 000007ff001701e8 System.IO.TextReader+SyncTextReader.ReadLine()
53     PARAMETERS:
54         this = 0x0000000002625a98
55 
56 00000000002cef50 000007fee82dc6a2 Test.Program.Main(System.String[])
57     PARAMETERS:
58         args = 0x0000000002623598
59     LOCALS:
60         0x00000000002cef70 = 0x0000000002623658
61         0x00000000002cef78 = 0x0000000002623678

當程序實例化兩個Student對象,執行完 Student lee = new Student("lee", "Shanghai") 後,
這裏保存了2個對象,嘿嘿:

59     LOCALS:
60         0x00000000002cef70 = 0x0000000002623658
61         0x00000000002cef78 = 0x0000000002623678

看到了,第一個對象的地址是:0x0000000002623658, 而前面說,第0代的起始地址是 0x0000000002621030, 很顯然,這兩個對象被分配在了0代,且佔用了0x20(32)個字節。

不相信,那咱們來驗明下正身,

0:000> .load C:\Symbols\sosex_64\sosex.dll
0:000> !gcgen 0x0000000002623658
GEN 0

呵呵,還要繼續驗身麼,繼續...

! do 0x0000000002623658,

0:000> !do 0x0000000002623658
Name: Test.Student
MethodTable: 000007ff00033538
EEClass: 000007ff001623a8
Size: 32(0x20) bytes
 (D:\Test\PInvoke\CPP\Test\bin\Debug\Test.exe)
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee7577d90  4000001        8        System.String  0 instance 00000000026235b8 <Name>k__BackingField
000007fee7577d90  4000002       10        System.String  0 instance 00000000026235e0 <Address>k__BackingField

驗到了, 就是Test.Student類的實例,Name和Address字段都看到了。

開個小差,看看這個實例的內容,

 !do 00000000026235b8,

0:000> !do 00000000026235b8 Name: System.String MethodTable: 000007fee7577d90 EEClass: 000007fee717e560 Size: 34(0x22) bytes (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: wang Fields: MT Field Offset Type VT Attr Value Name 000007fee757f000 4000096 8 System.Int32 1 instance 5 m_arrayLength 000007fee757f000 4000097 c System.Int32 1 instance 4 m_stringLength 000007fee75797d8 4000098 10 System.Char 1 instance 77 m_firstChar 000007fee7577d90 4000099 20 System.String 0 shared static Empty >> Domain:Value 0000000000bcb1d0:0000000002621308 << 000007fee7579688 400009a 28 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 0000000000bcb1d0:0000000002621a98 <<

 

0:000> !do 00000000026235e0 
Name: System.String
MethodTable: 000007fee7577d90
EEClass: 000007fee717e560
Size: 40(0x28) bytes
 (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Beijing
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee757f000  4000096        8         System.Int32  1 instance                8 m_arrayLength
000007fee757f000  4000097        c         System.Int32  1 instance                7 m_stringLength
000007fee75797d8  4000098       10          System.Char  1 instance               42 m_firstChar
000007fee7577d90  4000099       20        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000000bcb1d0:0000000002621308 <<
000007fee7579688  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                 >> Domain:Value  0000000000bcb1d0:0000000002621a98 <<
0:000> !do 00000000026235b8 

 果真, 是家住「beijing」的"wang" 同窗!

 

經過這個事例,咱們驗證了,在.net中,初始分配的小對象在0代上。

那麼假設,咱們執行GC.Collect(),結果會怎樣?

1  static void Main(string[] args)
2         {
3             Student wang = new Student("wang", "Beijing");
4             Student lee = new Student("lee", "Shanghai");
5 
6             GC.Collect(); 
7             Console.ReadLine();
8         }

不一步步看了,給明結果吧:

000000000032eb50 000007fee82dc6a2 Test.Program.Main(System.String[])
    PARAMETERS:
        args = 0x00000000025d3598
    LOCALS:
        0x000000000032eb70 = 0x00000000025d3658
        0x000000000032eb78 = 0x00000000025d3678

0:000> !gcgen 0x00000000025d3658
GEN 1
0:000> !gcgen 0x00000000025d3678
GEN 1

 

2 Dispose,Finalization(終結器)
 

Dispose:用於處置那些佔用非託管資源的對象。

Finalization(終結器): 這是CLR提供的一種機制,容許對象在GC回收其內存以前執行一些清理工做。

當客戶端記得的時候使用IDisposable接口釋放你的非受控資源,當客戶端忘記的時候防禦性地使用終結器(finalizer)。它與垃圾收集器(Garbage Collector)一塊兒工做,確保只在必要的時候該對象才受到與終結器相關的性能影響。這是處理非受控資源的一條很好的途徑,所以咱們應該完全地認識它。若是你的類使用了非內存資源,它就必須含有一個終結器。你不能依賴客戶端老是C#調用Dispose()方法。由於當它們忘記這樣作的時候,你就面臨資源泄漏的問題。沒有調用Dispose是它們的問題,可是你卻有過失。

用於保證非內存資源被正確地釋放的惟一途徑是建立終結器。

調用Dispose()方法的實現(implementation)負責下面四個事務:

1.釋放全部的非受控資源。

2.釋放全部的受控資源(包括未解開事件)。

3.設置標誌代表該對象已經被處理過了。你必須在本身的公共方法中檢查這種狀態標誌並拋出ObjectDisposed異常(若是某個對象被處理過以後再次被調用的話)。

4.禁止終結操做(finalization)。調用GC.SuppressFinalize(this)來完成這種事務。

 Finalization(終結器)原理:

 應用程序建立一個新對象時,new操做符會從堆中分配內存。若是這個對象定義了Finalize方法,那麼該類型的實例在構造器被調用以前,會將指向該對象的一個指針放到一個finalization list中。finalization list是由GC控制的一個內部數據結構。列表中的每一項都指向一個對象,在回收該對象的內存前,會調用他的Finalize方法。

GC開始時,假定某些對象(如B,C,D對象)被斷定位垃圾後,GC會掃描finalization list 以查找指向前述對象(如B,C,D對象)的指針。若發現finalization list有指針指向前述對象(如B,C,D對象)。finalization list會移除指向前述對象(如B,C,D對象)的指針,並把指針追加到Freachable隊列。

當垃圾回收器將對象的引用從finalization list移至freachable隊列時,對象再也不被視爲垃圾,其內存不能被回收,即對象「復活」。

而後,GC開始compact可回收內存,特殊的高優先級CLR線程專門負責清空freachable隊列,並調用finalize方法。

再次GC被調用時,會發現應用程序的根再也不指向它,freachable隊列也已經清空。因此,這些對象的內存會被直接回收。

整個過程當中,實現Finalization(終結器)的對象須要至少執行兩次垃圾回收才能釋放其所佔內存。(假設對象代齡被提高,則可能屢次GC纔回收其內存)。

 

規範的Dispose實現模式:

 1     public class ComplexCleanupBase : IDisposable
 2     {
 3         // some fields that require cleanup  
 4         private SafeHandle handle;
 5 
 6         private bool disposed = false; // to detect redundant calls
 7 
 8         public ComplexCleanupBase()
 9         {
10             // allocate resources
11         }
12 
13         protected virtual void Dispose(bool disposing)
14         {
15             if (!disposed)
16             {
17                 if (disposing)
18                 {
19                     if (handle != null)  
20                         handle.Dispose();
21                     // dispose-only, i.e. non-finalizable logic
22                 }
23 
24                 // shared cleanup logic
25                 disposed = true;
26             }
27         }
28 
29         public void Dispose()
30         {
31             Dispose(true);
32             GC.SuppressFinalize(this);
33         }
34 
35         ~ComplexCleanupBase()
36         {
37             Dispose(false);
38         } 
39     }

 

瞭解上述後,來看個問題:

a. 數據庫鏈接,文件鏈接假如不手動dispose(),資源會被回收麼?

回答上面這個問題前,先作個試驗,

 1  protected void Button1_Click(object sender, EventArgs e)
 2         {
 3             byte[] b = new byte[] { 1, 2, 3, 4, 5 };
 4             FileStream fs = new FileStream(@"d:\temp.dat", FileMode.Create);
 5             fs.Write(b, 0, b.Length);
 6 
 7         }
 8 
 9         protected void Button2_Click(object sender, EventArgs e)
10         {
11             GC.Collect();          
12         }
13 
14         protected void Button3_Click(object sender, EventArgs e)
15         {
16             File.Delete(@"d:\temp.dat");
17         }

Button1,Button2,Button3來回點幾下,看看會有什麼現象?

連續點擊Button1或先點Button1後點Buttion3,第二次都會報錯,說明文件鏈接沒有被釋放。可是若是點擊Button1再點擊Button2再點擊Button1那麼就不會報錯了,由於Button2垃圾回收將文件鏈接關閉了。  

這麼說來,數據庫鏈接,文件鏈接假如不手動dispose(),資源也會被GC回收,由於FileStream裏的SaveFileHandle實現了Finalize,執行GC,其Finalize方法起了做用。

好強大的GC哦! 只不過,須要等待GC.collect()才能釋放這些資源鏈接,可是呢這些資源開着開銷很大很昂貴,因此推薦用完即手動dispose().

這樣看規範的Dispose實現模式,可以確保.net資源即便被遺忘關閉,藉助垃圾回收機制,也能順利清理其資源。

 

好了,有了上面這些基礎以後,再來看一個例子:

 1  public class Foo
 2     {
 3         Timer _timer;
 4 
 5         public Foo()
 6         {
 7             _timer = new Timer(1000);
 8             _timer.Elapsed += _timer_Elapsed;
 9             _timer.Start();
10         }
11 
12         void _timer_Elapsed(object sender, ElapsedEventArgs e)
13         {
14             Console.WriteLine("Tick");
15         }
16     }
17     class Program
18     {
19         static void Main(string[] args)
20         {
21             Foo foo = new Foo();
22             foo = null; 
23             Console.ReadLine();
24         }
25     }

注:Timer類來自using System.Timers;
執行發現會出現一連串的 「Tick」。 這裏foo=null並未起到做用,Timer資源並未關閉。這裏咱們不深究爲什麼foo=null不起做用,其實是由於.net編譯作了優化處理,foo=null直接被編譯器忽視了。

那怎麼辦才能作到萬事順利地關閉Timer?

Dispose模式登場,修改後的類以下:

 1   public class Foo : IDisposable
 2     {
 3         Timer _timer;
 4 
 5         public Foo()
 6         {
 7             _timer = new Timer(1000);
 8             _timer.Elapsed += _timer_Elapsed;
 9             _timer.Start();
10         }
11 
12         void _timer_Elapsed(object sender, ElapsedEventArgs e)
13         {
14             Console.WriteLine("Tick");
15         }
16 
17         public void Dispose()
18         {
19             _timer.Dispose();
20         }
21         ~Foo()
22         {
23             Dispose();
24         }
25     }
26     class Program
27     {
28         static void Main(string[] args)
29         {
30             using (Foo foo = new Foo())
31             {
32                 System.Threading.Thread.Sleep(3000);
33             }
34             Console.ReadLine();
35         }
36     }
相關文章
相關標籤/搜索