原: http://www.mikel.cn/article.asp?id=1698
你清楚.Net的垃圾回收機制嗎?能簡述一下GC的工作原理嗎?怎麼樣才能有效的管理記憶體呢? Using語句體內實例化的物件有什麼作用?
本節的組織如下,1..Net的類型和記憶體分配2.GC垃圾收集器的工作原理3.什麼是非託管資源4.如何有效釋放物件資源。總結.現在開始我們本節的學習。
1..Net的類型和記憶體分配
Net中的所有類型都是(直接或間接)從System.Object 類型派生的。
CTS 中的型別被分成兩大類別-引用型別( reference type ,又叫代管型別[managed type] ),指派在記憶體堆上,值型別( value type )。值類型分配在堆疊上。如圖
值類型在堆疊裡,先進後出,值類型變數的生命有先後順序,這個確保了值類型變數在推出作用域以前會釋放資源。比引用類型更簡單和有效率。堆疊是從高位址往低位址分配記憶體。
引用型別分配在託管堆(Managed Heap)上,宣告一個變數在堆疊上保存,當使用new建立物件時,會把物件的位址儲存在這個變數裡。託管堆相反,從低位址往高位址分配內存,如圖
2.GC垃圾收集器的工作原理
上圖中,當dataSet使用過期以後,我們不顯示銷毀對象,堆上的對象仍繼續存在,等待GC的回收。
垃圾收集器透過分代支援對象的年齡化是建議的但不是必需的。一代在記憶體裡是一個相對年齡的物件的單位。對象的
代號或年齡標識對象屬於那個分代。在應用程式的生命週期裡,越近創建的物件屬於越新的代,並且比早創建的物件具有
較低的分代號。最近分代裡的物件代號是0.
在new物件時,要先搜尋空閒鍊錶,找到最適合記憶體區塊,分配,調整記憶體區塊鍊錶,合併碎片。 new操作幾乎可以在O(1)的時間完成,把堆頂指針加1。工作原理是: 當託管堆上剩餘空間不足,或Generator 0 的空間已滿的時候GC運行,開始回收記憶體。垃圾回收的開始,GC對堆記憶體的壓縮調整,物件集中到頂部。 GC在掃描垃圾的時候會佔用一定的CPU時間片的,最初的GC演算法真的是掃描整個堆,效率低。現在的GC把堆中的物件分成3代,最近進入堆的是第0代(generation 0), 其次是generation 1, generation2. 第一次GC只掃描第0代。如果回收的空間足夠當前使用就不必掃描其它generation的物件。所以,GC創建物件的效率比C++高效,不需要掃描全部堆空間。它透過掃描策略,再加上記憶體管理策略帶來的效能提升,足以補償GC所佔用的CPU時間。
3.什麼是非託管資源
常見的非託管資源就是包裝作業系統資源的對象,例如文件,視窗或網路連接,對於這類資源雖然垃圾回收器可以追蹤封裝非託管資源的對象的生存期,但它知道如何清理這些資源。還好.net Framework提供的Finalize()方法,它允許在垃圾回收器回收該類別資源前,適當的清理非託管資源。這裡列舉幾種常見的非託管資源:畫筆、流物件、元件物件等等資源(Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,ApplicationContext,Brush,
Component,ComponentDesigner,Container,Context,Cursor,FileStream,
Font,Icon,Image,Matrix,Timer,Tooltip)。 (參考MSDN)
4.如何有效釋放非託管資源。
GC無法管理非託管資源,那麼如何釋放非託管資源呢? .Net提供了兩種方式:
(1)析構函數:垃圾收集器回收非託管物件的資源時,會呼叫物件的終結方法Finalize(),進行資源的清理工作,但是由於GC工作規則的限制,GC呼叫物件的Finalize方法,第一次不會釋放資源,第二次呼叫之後才刪除物件。
(2)繼承IDisposable 介面,實作Dispose()方法,IDisposable介面定義了一個模式(具有語言層級的支援),為釋放未託管的資源提供了確定的機制,並避免產生析構函數固有的與垃圾收集器相關的問題。
為了更好的理解垃圾回收機制,我特地寫了部分程式碼,裡面添加了詳細的註解。定義單一類別FrankClassWithDispose(繼承介面IDisposable ) 、FrankClassNoFinalize(沒終結器) 、FrankClassWithDestructor(定義了析構函式)。
具體代碼如下:
Code
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4 using System.Data;
5 using System.Data.Odbc;
6 using System.Drawing;
7 // Coded By Frank Xu Lei 18/2/2009
8 // Study the .NET Memory Management
9 // Garbage Collector 垃圾收集器。可以根據策略在需要的時候回收託管資源,
10 // 但是GC不知道如何管理非託管資源。如網路連線、資料庫連線、畫筆、組件等
11 // 兩個機制來解決非託管資源的釋放問題。析構函數、IDispose介面
12 // COM引用計數
13 // C++手動管理,New Delete
14 // VB自動管理
15 namespace MemoryManagement
16 {
17 // 繼承介面IDisposable,實作Dispose方法,可以釋放FrankClassDispose的實例資源
18 public class FrankClassWithDispose : IDisposable
19 {
20 private OdbcConnection _odbcConnection = null ;
21
22 // 建構函式
23 public FrankClassWithDispose()
24 {
25 if (_odbcConnection == null )
26 _odbcConnection = new OdbcConnection();
27 Console.WriteLine( " FrankClassWithDispose has been created " );
28 }
29 // 測試方法
30 public void DoSomething()
31 {
32
33 /**/ /// /code here to do something
34 return ;
35 }
36 // 實作Dispose,釋放本類別所使用的資源
37 public void Dispose()
38 {
39 if (_odbcConnection != null )
40 _odbcConnection.Dispose();
41 Console.WriteLine( " FrankClassWithDispose has been disposed " );
42 }
43 }
44 // 沒有實作Finalize,等著GC回收FrankClassFinalize的實例資源,GC運行時候直接回收
45 public class FrankClassNoFinalize
46 {
47 private OdbcConnection _odbcConnection = null ;
48 // 建構函式
49 public FrankClassNoFinalize()
50 {
51 if (_odbcConnection == null )
52 _odbcConnection = new OdbcConnection();
53 Console.WriteLine( " FrankClassNoFinalize has been created " );
54 }
55 // 測試方法
56 public void DoSomething()
57 {
58
59 // GC.Collect();
60 /**/ /// /code here to do something
61 return ;
62 }
63 }
64 // 實作析構函數,編譯為Finalize方法,呼叫物件的析構函數
65 // GC運行時,兩次調用,第一次沒釋放資源,第二次才釋放
66 // FrankClassDestructor的實例資源
67 // CLR使用獨立的執行緒來執行物件的Finalize方法,頻繁呼叫會使效能下降
68 public class FrankClassWithDestructor
69 {
70 private OdbcConnection _odbcConnection = null ;
71 // 建構函數
72 public FrankClassWithDestructor()
73 {
74 if (_odbcConnection == null )
75 _odbcConnection = new OdbcConnection();
76 Console.WriteLine( " FrankClassWithDestructor has been created " );
77 }
78 // 測試方法
79 public void DoSomething()
80 {
81 /**/ /// /code here to do something
82
83 return ;
84 }
85 // 析構函數,釋放未託管資源
86 ~ FrankClassWithDestructor()
87 {
88 if (_odbcConnection != null )
89 _odbcConnection.Dispose();
90 Console.WriteLine( " FrankClassWithDestructor has been disposed " );
91 }
92 }
93 }
94
其中使用了非託管的物件OdbcConnection 的實例。建立的客戶端進行了簡單的測試。客戶端程式碼如下:
Code
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4 using System.Data;
5 using MemoryManagement;
6 // Coded By Frank Xu Lei 18/2/2009
7 // Study the .NET Memory Management
8 // Test The Unmanaged Objects Reclaimed.
9 // 針對非託管程式碼的測試,比較
10 // 託管程式碼,GC可以更具策略自行回收,也可以實作IDisposable,呼叫Dispose()方法,主動釋放。
11 namespace MemoryManagementClient
12 {
13 class Program
14 {
15 static void Main( string [] args)
16 {
17
18 /**/ /////////////////////////////////////// //(1) / ///////////////////////////////////////// //
19 // 呼叫Dispose()方法,主動釋放。資源,靈活
20 FrankClassWithDispose _frankClassWithDispose = null ;
21 try
22 {
23 _frankClassWithDispose = new FrankClassWithDispose();
24 _frankClassWithDispose.DoSomething();
25
26 }
27 finally
28 {
29 if (_frankClassWithDispose != null )
30 _frankClassWithDispose.Dispose();
31 // Console.WriteLine("FrankClassWithDispose實例已經被釋放");
32 }
33
34 /**/ /////////////////////////////////////// //(2) / //////////////////////////////////////////////
35 // 可以使用Using語句建立非託管物件,方法執行結束前,會呼叫
36 using (FrankClassWithDispose _frankClassWithDispose2 = 新 FrankClassWithDispose())
37 {
38 // _frankClassWithDispose2.DoSomething();
39 }
40
41 /**/ /////////////////////////////////////// //(3) / ///////////////////////////////////////// //
42 // 垃圾收集器運作的時候,一次就釋放資源
43 FrankClassNoFinalize _frankClassNoFinalize = new FrankClassNoFinalize();
44 _frankClassNoFinalize.DoSomething();
45
46 /**/ ////////////////////////////////////////// (4) ///////////////////////////////////////////// /
47 // 垃圾收集器運作的時候,兩次才能夠釋放資源
48 FrankClassWithDestructor _frankClassWithDestructor = new FrankClassWithDestructor();
49 _frankClassWithDestructor.DoSomething();
50 /**/ ///////////////////////////////////////////(5 ) /////////////////////////////////////////////
51 // 不能使用Using語句來建立對象,因為其沒實作IDispose介面
52 // using (FrankClassWithDestructor _frankClassWithDestructor2 = new FrankClassWithDestructor())
53 // {
54 // _frankClassWithDestructor2.DoSomething();
55 // }
56
57 /**/ ///////////////////////////////////////////// /////////////////////////////////////// //
58 // For Debug
59 Console.WriteLine( " Press any key to continue " );
60 Console.ReadLine();
61
62
63 }
64 }
65 }
66
有些時候資源必須在特定時間釋放,類別可以實作執行資源管理和清除任務方法IDisposable.Dispose的介面IDisposable。
如果呼叫者需要呼叫Dispose方法清理對象,類別作為契約的一部分必須實作Dispose方法。垃圾收集器預設不會調用
Dispose方法;然而,實作Dispose方法可以呼叫GC裡的方法去規範垃圾收器的終結行為。
值得一提的是:呼叫Dispose()方法,主動釋放資源,靈活,可以使用Using語句建立非託管物件,方法執行結束前,會調用
Dispose()方法釋放資源, 這兩端程式碼的效果是一樣的,可以查看編譯後IL。
Code
1 . try
2 {
3 IL_0003: nop
4 IL_0004: newobj instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
5 IL_0009: stloc. 0
6 IL_000a: ldloc. 0
7 IL_000b: callvirt instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::DoSomething()
8 IL_0010: nop
9 IL_0011: nop
10 IL_0012: leave.s IL_0028
11 } // end .try
12 finally
13 {
14 IL_0014: nop
15 IL_0015: ldloc. 0
16 IL_0016: ldnull
17 IL_0017: ceq
18 IL_0019: stloc.s CS$ 4 $ 0000
19 IL_001b: ldloc.s CS$ 4 $ 0000
20 IL_001d: brtrue.s IL_0026
21 IL_001f: ldloc. 0
22 IL_0020: callvirt instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::Dispose()
23 IL_0025: nop
24 IL_0026: nop
25 IL_0027: endfinally
26 } // end handler
27 IL_0028: nop
28 IL_0029: newobj instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
29 IL_002e: stloc. 1
30 . try
31 {
32 IL_002f: nop
33 IL_0030: nop
34 IL_0031: leave.s IL_0045
35 } // end .try
36 finally
37 {
38 IL_0033: ldloc. 1
39 IL_0034: ldnull
40 IL_0035: ceq
41 IL_0037: stloc.s CS$ 4 $ 0000
42 IL_0039: ldloc.s CS$ 4 $ 0000
43 IL_003b: brtrue.s IL_0044
44 IL_003d: ldloc. 1
45 IL_003e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
46 IL_0043: nop
47 IL_0044: endfinally
48 } // end handler
49
Using 語句有相同的效果,來實現非託管物件資源的釋放。這點在面試中也會常常遇到,Using關鍵字的用法有哪幾種等等類似的問題。基本理想的答案都是除了引用命名空間,和命名空間設定別名外,就是這個用法實現如try finally區塊一樣作用的對非託管物件資源的回收。只是一種簡單的寫法。
當你用Dispose方法釋放未託管物件的時候,應該呼叫GC.SuppressFinalize。如果物件正在終結佇列(finalization queue),GC.SuppressFinalize會阻止GC呼叫Finalize方法。因為Finalize方法的呼叫會犧牲部分效能。如果你的Dispose方法已經對委託管資源作了清理,就沒必要讓GC再呼叫物件的Finalize方法(MSDN)。附上MSDN的代碼, 大家可以參考.
Code
public class BaseResource: IDisposable
{
// 指向外部非託管資源
private IntPtr handle;
// 此類使用的其它託管資源.
private Component Components;
// 追蹤是否呼叫.Dispose方法,標識位,控制垃圾收集器的行為
private bool disposed = false ;
// 建構函式
public BaseResource()
{
// Insert appropriate constructor code here.
}
// 實作介面IDisposable.
// 不能宣告為虛方法virtual.
// 子類別不能重寫這個方法.
public void Dispose()
{
Dispose( true );
// 離開終結佇列Finalization queue
// 設定物件的阻止終結器程式碼
//
GC.SuppressFinalize( this );
}
// Dispose(bool disposing) 執行分兩種不同的情況.
// 如果disposing 等於true, 方法已經被呼叫
// 或間接被使用者程式碼呼叫. 託管和非託管的程式碼都能被釋放
// 如果disposing 等於false, 方法已經被終結器finalizer 從內部呼叫過,
// 你就不能引用其他對象,只有非託管資源可以被釋放。
protected virtual void Dispose( bool disposing)
{
// 檢查Dispose 是否被呼叫過.
if ( ! this .disposed)
{
// 如果等於true, 釋放所有託管和非託管資源
if (disposing)
{
// 釋放託管資源.
Components.Dispose();
}
// 釋放非託管資源,如果disposing為false,
// 只會執行下面的程式碼.
CloseHandle(handle);
handle = IntPtr.Zero;
// 注意這裡是非線程安全的.
// 在託管資源釋放以後可以啟動其它線程銷毀對象,
// 但是在disposed標記設定為true前
// 如果執行緒安全是必須的,客戶端必須實作。
}
disposed = true ;
}
// 使用interop 呼叫方法
// 清除非託管資源.
[System.Runtime.InteropServices.DllImport( " Kernel32 " )]
private extern static Boolean CloseHandle(IntPtr handle);
// 使用C# 析構函數來實現終結器程式碼
// 這個只在Dispose方法沒被呼叫的前提下,才能呼叫執行。
// 如果你給基類終結的機會.
// 不要給子類別提供析構函數.
~ BaseResource()
{
// 不要重複建立清理的程式碼.
// 基於可靠性和可維護性考慮,呼叫Dispose(false) 是最佳的方式
Dispose( false );
}
// 允許你多次呼叫Dispose方法,
// 但是會拋出異常如果物件已經釋放。
// 不論你什麼時間處理物件都會檢查物件的是否釋放,
// check to see if it has been disposed.
public void DoSomething()
{
if ( this .disposed)
{
throw new ObjectDisposedException();
}
}
對於需要呼叫Close方法比Dispose方法更自然的型別,可以在 基底類別增加一個Close方法。
Close方法無參呼叫執行適當清理工作的Dispose方法。
下面的例子示範了Close方法。
// 不要設定方法為virtual.
// 繼承類別不允許重寫這個方法
public void Close()
{
// 無參數呼叫Dispose參數.
Dispose();
}
public static void Main()
{
// Insert code here to create
// and use a BaseResource object.
}