有些對象需要顯示地銷毀代碼來釋放資源,比如打開的文件資源,鎖,操作系統句柄和非托管對象。在.NET中,這就是所謂的對象銷毀,它通過IDisposal介面來實現。不再使用的對象所占用的記憶體管理,必須在某個時候回收;這個被稱為無用單元收集的功能由CLR執行。 對象銷毀和垃圾回收的區別在於:對象銷毀通常是 ...
有些對象需要顯示地銷毀代碼來釋放資源,比如打開的文件資源,鎖,操作系統句柄和非托管對象。在.NET中,這就是所謂的對象銷毀,它通過IDisposal介面來實現。不再使用的對象所占用的記憶體管理,必須在某個時候回收;這個被稱為無用單元收集的功能由CLR執行。
對象銷毀和垃圾回收的區別在於:對象銷毀通常是明確的策動;而垃圾回收完全是自動地。換句話說,程式員負責釋放文件句柄,鎖,以及操作系統資源;而CLR負責釋放記憶體。
本章將討論對象銷毀和垃圾回收,還描述了C#處理銷毀的一個備選方案--Finalizer及其模式。最後,我們討論垃圾回收器和其他記憶體管理選項的複雜性。
對象銷毀 | 垃圾回收 |
1)IDisposal介面 2) Finalizer |
垃圾回收 |
對象銷毀用於釋放非托管資源 | 垃圾回收用於自動釋放不再被引用的對象所占用的記憶體;並且垃圾回收什麼時候執行時不可預計的 |
為了彌補垃圾回收執行時間的不確定性,可以在對象銷毀時釋放托管對象占用的記憶體 |
IDisposal,Dispose和Close
.NET Framework定義了一個特定的介面,類型可以使用該介面實現對象的銷毀。該介面的定義如下:
public interface IDisposable { void Dispose(); }
C#提供了鴘語法,可以便捷的調用實現了IDisposable的對象的Dispose方法。比如:
using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open)) { // ... Write to the file ... }
編譯後的代碼與下麵的代碼是一樣的:
FileStream fs = new FileStream ("myFile.txt", FileMode.Open); try { // ... Write to the file ... } finally { if (fs != null) ((IDisposable)fs).Dispose(); }
finally語句確保了Dispose方法的調用,及時發生了異常,或者代碼在try語句中提前返回。
在簡單的場景中,創建自定義的可銷毀的類型值需要實現IDisposable介面即可
sealed class Demo : IDisposable { public void Dispose() { // Perform cleanup / tear-down. ... } }
請註意,對於sealed類,上述模式非常適合。在本章後面,我們會介紹另外一種銷毀對象的模式。對於非sealed類,我們強烈建議時候後面的那種銷毀對象模式,否則在非sealed類的子類中,也希望實現銷毀時,會發生非常詭異的問題。
對象銷毀的標準語法
Framework在銷毀對象的邏輯方面遵循一套規則,這些規則並不限用於.NET Framework或C#語言;這些規則的目的是定義一套便於使用的協議。這些協議如下:
- 一旦銷毀,對象不可恢復。對象不能被再次激活,調用對象的方法或者屬性拋出ObjectDisposedException異常
- 重覆地調用對象的Disposal方法會導致錯誤
- 如果一個可銷毀對象x包含,或包裝,或處理另外一個可銷毀對象y,那麼x的Dispose方法自動調用x的Dispose方法,除非另有指令(不銷毀y)
這些規則同樣也適用於我們平常創建自定義類型,儘管它並不是強制性的。沒有誰能阻止你編寫一個不可銷毀的方法;然而,這麼做,你的同事也許會用高射炮攻擊你。
對於第三條規則,一個容器對象自動銷毀其子對象。最好的一個例子就是,windows容器對象比如Form對著Panel。一個容器對象可能包含多個子控制項,那你也不需要顯示地銷毀每個字對象:關閉或銷毀父容器會自動關閉其子對象。另外一個例子就是如果你在DeflateStream包裝了FileStream,那麼銷毀DeflateStream時,FileStream也會被銷毀--除非你在構造器中指定了其他的指令。
Close和Stop
有一些類型除了Dispose方法之外,還定義了Close方法。Framework對於Close方法並沒有保持完全一致性,但在幾乎所有情況下,它可以:
- 要麼在功能上與Dispose一致
- 或只是Dispose的一部分功能
對於後者一個典型的例子就是IDbConnecton類型,一個Closed的連接可以再次被打開;而一個Disposed的連接對象則不能。另外一個例子就是Windows程式使用ShowDialog的激活某個視窗對象:Close方法隱藏該視窗;而Dispose釋放視窗所使用的資源。
有一些類定義Stop方法(比如Timer或HttpListener)。與Dipose方法一樣,Stop方法可能會釋放非托管資源;但是與Dispose方法不同的是,它允許重新啟動。
何時銷毀對象
銷毀對象應該遵循的規則是“如有疑問,就銷毀”。一個可以被銷毀的對象--如果它可以說話--那麼將會說這些內容:
“如果你結束對我的使用,那麼請讓我知道。如果只是簡單地拋棄我,我可能會影響其他實例對象、應用程式域、電腦、網路、或者資料庫”
如果對象包裝了非托管資源句柄,那麼經常會要求銷毀,以釋放句柄。例子包括Windows Form控制項、文件流或網路流、網路sockets,GDI+畫筆、GDI+刷子,和bitmaps。與之相反,如果一個類型是可銷毀的,那麼它會經常(但不總是)直接或間接地引用非托管句柄。這是由於非托管句柄對操作系統資源,網路連接,以及資料庫鎖之外的世界提供了一個網關(出入口),這就意味著使用這些對象時,如果不正確的銷毀,那麼會對外面的世界代碼麻煩。
但是,遇到下麵三種情形時,不要銷毀對象
- 通過靜態成員或屬性獲取一個共用的對象
- 如果一個對象的Dispose方法與你的期望不一樣
- 從設計的角度看,如果一個對象的Dispose方法不必要,且銷毀對象給程式添加了複雜度
第一種情況很少見。多數情形都可以在System.Drawing命名空間下找到:通過靜態成員或屬性獲取的GDI+對象(比如Brushed.Blue)就不能銷毀,這是因為該實現在程式的整個生命周期中都會用到。而通過構造器得到的對象實例,比如new SolidBrush,就應該銷毀,這同樣適用於通過靜態方法獲取的實例對象(比如Font.FromHdc)。
第二種情況就比較常見。下表以System.IO和System.Data命名空間下類型舉例說明
類型 | 銷毀功能 | 何時銷毀 |
MemoryStream | 防止對I/O繼續操作 | 當你需要再次讀讀或寫流 |
StreamReader, StreamWriter |
清空reader/writer,並關閉底層的流 | 當你希望底層流保持打開時(一旦完成,你必須改為調用StreamWriter的Flush方法) |
IDbConnection | 釋放資料庫連接,並清空連接字元串 | 如果你需要重新打開資料庫連接,你需要調用Close方法而不是Dispose方法 |
DataContext (LINQ to SQL) |
防止繼續使用 | 當你需要延遲評估連接到Context的查詢 |
第三者情況包含了System.ComponentModel命名空間下的這幾個類:WebClient, StringReader, StringWriter和BackgroundWorker。這些類型有一個共同點,它們之所以是可銷毀的是源於它們的基類,而不是真正的需要進行必要的清理。如果你需要在一個方法中使用這樣的類型,那麼在using語句中實例化它們就可以了。但是,如果實例對象需要持續一段較長的時間,並記錄何時不再使用它們以銷毀它們,就會給程式帶來不惜要的複雜度。在這樣的情況下,那麼你就應該忽略銷毀對象。
選擇性地銷毀對象
正因為IDisposable實現類可以使用using語句來實例化,因而這可能很容易導致該實現類的Dispose方法延伸至不必要的行為。比如:
public sealed class HouseManager : IDisposable { public void Dispose() { CheckTheMail(); } ... }
想法是該類的使用者可以選擇避免不必要的清理--簡單地說就是不調用Dispose方法。但是,這就需要調用者知道HouseManager類Dispose方法的實現細節。及時是後續添加了必要的清理行為也破壞了規則。
public void Dispose() { CheckTheMail(); // Nonessential LockTheHouse(); // Essential }
在這種情況下,就應該使用選擇性銷毀模式
public sealed class HouseManager : IDisposable { public readonly bool CheckMailOnDispose; public Demo (bool checkMailOnDispose) { CheckMailOnDispose = checkMailOnDispose; } public void Dispose() { if (CheckMailOnDispose) CheckTheMail(); LockTheHouse(); } ... }
這樣,任何情況下,調用者都可以調用Dispose--上述實現不僅簡單,而且避免了特定的文檔或通過反射查看Dispose的細節。這種模式在.net中也有實現。System.IO.Compression空間下的DeflateStream類中,它的構造器如下
public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)
非必要的行為就是在銷毀對象時關閉內在的流(第一個參數)。有時候,你希望內部流保持打開的同時並銷毀DeflateStream以執行必要的銷毀行為(清空bufferred數據)
這種模式看起來簡單,然後直到Framework 4.5,它才從StreamReader和StreamWriter中脫離出來。結果卻是醜陋的:StreamWriter必須暴露另外一個方法(Flush)以執行必要的清理,而不是調用Dispose方法(Framework 4.5在這兩個類上公開一個構造器,以允許你保持流處於打開狀態)。System.Security.Cryptography命名空間下的CryptoStream類,也遭遇了同樣的問題,當需要保持內部流處於打開時你要調用FlushFinalBlock銷毀對象。
銷毀對象時清除欄位
在一般情況下,你不要在對象的Dispose方法中清除該對象的欄位。然而,銷毀對象時,應該取消該對象在生命周期內所有訂閱的事件。退訂這些事件避免了接收到非期望的通知--同時也避免了垃圾回收器繼續對該對象保持監視。
設置一個欄位用以指明對象是否銷毀,以便在使用者在該對象銷毀後訪問該對象拋出一個ObjectDisposedException,這是非常值得做的。一個好的模式就是使用一個public的制度的屬性:
public bool IsDisposed { get; private set; }
儘管技術上沒有必要,但是在Dispose方法清除一個對象所擁有的事件句柄(把句柄設置為null)也是非常好的一種實踐。這消除了在銷毀對象期間這些事件被觸發的可能性。
偶爾,一個對象擁有高度秘密,比如加密密鑰。在這種情況下,那麼在銷毀對象時清除這樣的欄位就非常有意義(避免被非授權組件或惡意軟體發現)。System.Security.Cryptography命令空間下的SymmetricAlgorithm類就屬於這種情況,因此在銷毀該對象時,調用Array.Clear方法以清除加密密鑰。
自動垃圾回收機制
無論一個對象是否需要Dispose方法以實現銷毀對象的邏輯,在某個時刻,該對象在堆上所占用的記憶體空間必須釋放。這一切都是由CLR通過GC自動處理. 你不需要自己釋放托管記憶體。我們首先來看下麵的代碼
public void Test() { byte[] myArray = new byte[1000]; }
當Test方法執行時,在記憶體的堆上分配1000位元組的一個數組;該數組被變數myArray引用,這個變數存儲在變數棧上。當方法退出後,局部變數myArray就失去了存在的範疇,這也意味著沒有引用指向記憶體堆上的數組。那麼該孤立的數組,就非常適合通過垃圾回收機制進行回收。
垃圾回收機制並不會在一個對象變成孤立的對象之後就立即執行。與大街上的垃圾收集不一樣,.net垃圾回收是定期執行,盡享不是按照一個估計的計劃。CLR決定何時進行垃圾回收,它取決於許多因素,比如,剩餘記憶體,已經分配的記憶體,上一次垃圾回收的時間。這就意味著,在一個對象被孤立後到期占用的記憶體被釋放之間,有一個不確定的時間延遲。該延遲的範圍可以從幾納秒到數天。
垃圾回收和記憶體占用 垃圾收集試圖在執行垃圾回收的時間與程式的記憶體占用之間建立一個平衡。因此,程式可以占用比它們實際需要更多的記憶體,尤其特現在程式創建的大的臨時數組。 你可以通過Windows任務管理器監視某一個進程記憶體的占用,或者通過編程的方式查詢性能計數器來監視記憶體占用: // These types are in System.Diagnostics: string procName = Process.GetCurrentProcess().ProcessName; using (PerformanceCounter pc = new PerformanceCounter ("Process", "Private Bytes", procName)) Console.WriteLine (pc.NextValue());上面的代碼查詢內部工作組,返回你當前程式的記憶體占用。尤其是,該結果包含了CLR內部釋放,以及把這些資源讓給操作系統以供其他的進程使用。 |
根
根就是指保持對象依然處於活著的事物。如果一個對象不再直接或間接地被一個根引用,那麼該對象就適合於垃圾回收。
一個跟可以是:
- 一個正在執行的方法的局部變數或參數(或者調用棧中任意方法的局部變數或參數)
- 一個靜態變數
- 存貯在結束隊列中的一個對象
正在執行的代碼可能涉及到一個已經刪除的對象,因此,如果一個實例方法正在執行,那麼該實例方法的對象必然按照上述方式被引用。
請註意,一組相互引用的對象的迴圈被視作無根的引用。換一種方式,也就是說,對象不能通過下麵的箭頭指向(引用)而從根獲取,這也就是引用無效,因此這些對象也將被垃圾回收器處理。
Finalizers
在一個對象從記憶體釋放之前,如果對象包含finalizer,那麼finalizer開始運行。一個finalizer的聲明類似構造器函數,但是它使用~首碼符號
class Test { ~Test() { // finalizer logic ... } }
(儘管與構造器的聲明相似,finalizer不能被聲明為public或static,也不能有參數,還不能調用其基類)
Finalizer是可能的,因為垃圾收集工作在不同的時間段。首先,垃圾回收識別沒有使用的對象以刪除該對象。這些待刪除的對象如果沒有Finalizer那麼就立即刪除。而那些擁有finalizer的對象會被保持存活並存在放到一個特殊的隊列中。
在這一點上,當你的程式在繼續執行的時候,垃圾收集也是完整的。而Finalizer線程卻在你程式運行時,自動啟動併在另外一個線程中併發執行,收集擁有Finalizer的對象到特殊隊列,然後執行它們的終止方法。在每個對象的finalizer方法執行之前,它依然非常活躍--排序行為視作一個跟對象。而一檔這些對象被移除隊列,並且這些對象的fainalizer方法已經執行,那麼這些對象就變成孤立的對象,會在下一階段的垃圾回收過程中被回收。
Finalizer非常有用,但它們也有一些限制:
- Finalizer減緩記憶體分配和收集(因為GC需要追蹤那些Finalizer在運行)
- Finalizer延長對象及其所引用對象的生命周期(這些對象只有在下一次垃圾回收運行過程中被真正地刪除)
- 對於一組對象,Finalizer的調用順序是不可預測的
- 你不能控制一個對象的finalizer何時被調用
- 如果一個對象的finalizer被阻塞,那麼其他對象不能處置(Finalized)
- 如果程式沒有卸載(unload)乾凈,那麼finalizer會被忽略
總之,finalizer在一定程度上就好比律師--一旦有訴訟那麼你確實需要他們,一般你不想使用他們,除非萬不得已。如果你使用他們,那麼你需要100%確保你瞭解他們會為你做什麼。
下麵是實施finalizer的一些準則:
- 確保finalizer快速執行
- 絕對不要在finalier中使用阻塞
- 不要引用其他可finalizable對象
- 不要拋出異常
在Finalizer中調用Dispose
一個流行的模式是使finalizer調用Dispose方法。這麼做是有意義的,尤其是當清理工作不是緊急的,並且通過調用Dispose加速清理;那麼這樣的方式更多是一個優化,而不是一個必須。
下麵的代碼展示了該模式是如何實現的
class Test : IDisposable { public void Dispose() // NOT virtual { Dispose (true); GC.SuppressFinalize (this); // Prevent finalizer from running. } protected virtual void Dispose (bool disposing) { if (disposing) { // Call Dispose() on other objects owned by this instance. // You can reference other finalizable objects here. // ... } // Release unmanaged resources owned by (just) this object. // ... } ˜Test() { Dispose (false); } }
Dispose方法被重載,並且接收一個bool類型參數。而沒有參數的Dispose方法並沒有被聲明為virtual,只是在該方法內部調用了帶參數的Dispose方法,且傳遞的參數的值為true。
帶參數的Dispose方法包含了真正的處置對象的邏輯,並且它被聲明為protected和virtual。這樣就可以保證其子類可以添加自己的處置邏輯。參數disposing標記意味著它在Dispose方法中被正確的調用,而不是從finalizer的最後採取模式所調用。這也就表明,如果調用Dispose時,其參數disposing的值如果為false,那麼該方法,在一般情況下,都會通過finalizer引用其他對象(因為,這樣的對象可能自己已經被finalized,因此處於不可預料的狀態)。這裡面涉及的規則非常多!當disposing參數是false時,在最後採取的模式中,仍然會執行兩個任務:
釋放對操作系統資源的直接引用(這些引用可能是因為通過P/Invoke調用Win32 API而獲取到)
刪除由構造器創建的臨時文件
為了使這個模式更強大,那麼任何會拋出異常的代碼都應包含在一個try/catch代碼塊中;而且任何異常,在理想狀態下,都應該被記錄。此外,這些記錄應當今可能既簡單又強大。
請註意,在無參數的Dispose方法中,我們調用了GC.SuppressFinalize方法,這會使得GC在運行時,阻止finalizer執行。從技術角度講,這沒有必要,因為Dispose方法必然會被重覆調用。但是,這麼做會改進性能,因為它允許對象(以及它所引用的對象)在單個迴圈中被垃圾回收器回收。
複活
假設一個finalizer修改了一個活的對象,使其引用了一個“垂死”對象。那麼當下一次垃圾回收發生時,CLR會查看之前垂死的對象是否確實沒有任何引用指向它--從而確定是否對其執行垃圾回收。這是一個高級的場景,該場景被稱作複活(resurrection)。
為了證實這點,假設我們希望創建一個類管理一個臨時文件。當類的實例被回收後,我們希望finalizer刪除臨時文件。這看起來很簡單
public class TempFileRef { public readonly string FilePath; public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { File.Delete (FilePath); } }
實際,上訴代碼存在bug,File.Delete可能會拋出一個異常(引用缺少許可權,或者文件處於使用中) 。這樣的異常會導致拖垮整個程式(還會阻止其他finalizer執行)。我們可以通過一個空的catch代碼塊來“消化”這個異常,但是這樣我們就不能獲取任何可能發生的錯誤。 調用其他的錯誤報告API也不是我們所期望的,因為這麼做會加重finalizer線程的負擔,並且會妨礙對其他對象進行垃圾回收。 我們期望顯示finalization行為簡單、可靠、並快速。
一個好的解決方法是在一個靜態集合中記錄錯誤信息:
public class TempFileRef { static ConcurrentQueue<TempFileRef> _failedDeletions = new ConcurrentQueue<TempFileRef>(); public readonly string FilePath; public Exception DeletionError { get; private set; } public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { try { File.Delete (FilePath); } catch (Exception ex) { DeletionError = ex; _failedDeletions.Enqueue (this); // Resurrection } } }
把對象插入到靜態隊列_failedDeletions中,使得該對象處於引用狀態,這就確保了它仍然保持活著的狀態,直到該對象最終從隊列中出列。
GC.ReRegisterForFinalize
一個複活對象的finalizer不會再次運行--除非你調用GC.ReRegisterForFinalize
在下麵的例子中,我們試圖在一個finalizer中刪除一個臨時文件。但是如果刪除失敗,我們就重新註冊帶對象,以使其在下一次垃圾回收執行過程中被回收。
public class TempFileRef { public readonly string FilePath; int _deleteAttempt; public TempFileRef (string filePath) { FilePath = filePath; } ~TempFileRef() { try { File.Delete (FilePath); } catch { if (_deleteAttempt++ < 3) GC.ReRegisterForFinalize (this); } } }
如果第三次嘗試失敗後,finalizer會靜悄悄地放棄刪除臨時文件。我們可以結合上一個例子增強該行為--換句話說---那就是在第三次失敗後,把該對象加入到_failedDeletions隊列中。
垃圾回收工作原理
標準的CLR使用標記和緊湊的GC對存儲托管堆上的對象執行自動記憶體管理。GC可被視作一個可被追蹤的垃圾回收器,在這個回收器中,它(GC)不與任何對象接觸;而是被間歇性地被喚醒,然後跟蹤存儲在托管堆對象圖,以確定哪些對象可以被視為垃圾,進而對這些對象執行垃圾回收。
當(通過new關鍵字)執行記憶體分配是,或當已經分配的記憶體達到了某一閥值,亦或當應用程式占用的記憶體減少時,GC啟動一個垃圾收集。這個過程也可以通過手動調用System.GC.Collect方法啟動。在一個垃圾回收過程中,所有線程都可能被凍結。
GC從根對象引用開始,查找貴根對象對應的整個對象圖,然後把所有的對象標記為可訪問的對象。一旦這個過程完成,所有被標記為不再使用的對象,將被垃圾回收器回收。
沒有finalizer的不再使用的對象立即被處置;而擁有finalizer的不再使用對象將會在GC完成之後,在finalizer線程上排隊以等待處理。這些對象(在finalizer線程上排隊的對象)會在下一次垃圾回收過程中被回收(除非它們又複活了)。
而那些剩餘的“活”對象(還需要使用的對象),被移動到堆疊開始位置(壓縮),這樣以騰出更多空間容納更多對象。改壓縮過程有兩個目的:其一是避免了記憶體碎片,這樣就使得在為新對象分配空間後,GC只需使用簡單的策略即可,因為新的對象總是分配在堆的尾部。其二就是避免了維護一個非常耗時的記憶體片段列表任務。
在執行完一次垃圾回收之後,為新對象分配記憶體空間時,如果沒有足夠的空間可以使用,操作系統不能確保更多的記憶體使用時,拋出OutOfMemoryException。
優化技術
GC引入了各種優化技術來減少垃圾回收的時間。
通用垃圾回收
最重要的優化就是垃圾回收時通用的。其優點是:儘管快速分配和處置大量對象,某些對象是長存記憶體,因此他們不需要被垃圾回收追蹤。
基本上,GC把托管堆分為三類:Gen0是在堆上剛剛分配的對象;Gen1經過一次垃圾回收後仍然存活的對象;剩餘的為Gen2。
CLR限制Gen0的大小(在32位CLR中,最大16MB,一般大小為數百KB到幾MB)。當Gen0空間耗盡,GC便觸發一個Gen0垃圾回收--該垃圾回收發生非常頻繁。對於Gen1,GC也應用了一個相似的大小限制,因為Gen1垃圾回收也是相當頻繁並且快速完成。Gen2包含了所有類型的垃圾回收,然而,發生在Gen2的垃圾回收執行時間長,並且也不會經常發生。下圖展示了一個完全垃圾回收:
如果真要列出一組大概的數字,那麼Gen0垃圾回收執行耗費少於1毫秒,在一個應用程式中一般不會被註意到。而全垃圾回收,如果程式包含大的圖形對象,則可能會耗費100毫秒。執行時間受諸多因素影響二次可能會有不同,尤其是Gen2的垃圾回收,它的尺寸是沒有限定的。
段時間存活的對象,如果使用GC會非常有效。比如下麵示例代碼中的StringBuilder,就會很快地被髮生在Gen0上的垃圾回收所回收。
string Foo() { var sb1 = new StringBuilder ("test"); sb1.Append ("..."); var sb2 = new StringBuilder ("test"); sb2.Append (sb1.ToString()); return sb2.ToString(); }
大對象堆
GC為大對象(大小超過85,000位元組)使用單獨的堆。這就避免了大量消耗Gen0堆。因為在Gen0上沒有大對象,那麼就不會出現分配一組16MB的對象(這些對象由大對象組成)之後,馬上觸發垃圾回收。
大對象堆不適合於壓縮,這是因為發生垃圾回收時,移動記憶體大塊的代價非常高。如果這麼做,會帶來下麵兩個後果:
- 記憶體分配低效,這是因為GC不能總是把對象分配在堆的尾部,它還必須查看中間的空隙,那麼這就要求維護一個空白記憶體塊鏈表。
- 大對象堆適合於片段化。這意味著凍結一個對象,會在大對象堆上生成一個空洞,這個空洞很難在再被填充。比如,一個空洞留下了86000位元組的空間,那麼這個空間就只能被一個85000位元組或86000自己的對象填充(除非與另外的一個空洞連接在一起,形成更大的空間)
大對象堆還是非通用的堆,大對象堆上的所有對象被視作Gen2
併發回收和後臺回收
GC在執行垃圾回收時,必須釋放(阻塞)你的程式所使用的線程。在這個期間包含了Gen0發生的時間和Gen1發生的時間。
由於執行Gen2回收可能占用較長的時間,因此GC會在你的程式運行時,堆Gen2回收進行特殊的嘗試。該優化技術僅應用於工作站的CLR平臺,一般應用於windows桌面系統(以及所有運行獨立程式的Windows)。原因是由於阻塞線程進行垃圾回收所帶來的延遲對於沒有用戶介面的伺服器應用程式一般不會帶來問題。
這種對於工作站的優化歷史上稱之為併發回收。從CLR4.0kaishi ,它發生了革新並重命名為後臺回收。後臺回收移除了一個限制,由此,併發回收不再是併發的,如果Gen0部分已經執行完而Gen2回收還正在執行。這就意味著,從CLR4.0開始,持續分配記憶體的應用程式會更加敏感。
GC通知(適用於服務端CLR)
從Framework 3.5 SP1開始,伺服器版本的CLR在一個全GC將要發生時,向你發送通知。你可以在伺服器池配置中配置該特性:在一個垃圾回收執行之前,把請求轉向到另外一臺伺服器。然後你立即調查垃圾回收,並等待其完成,在垃圾回收執行完成之後,把請求轉回到當前伺服器。
通過調用GC.RegisterForFullGCNotification,可以啟用GC通知。然後,啟動另外一個線程,該線程首先調用GC.WaitForFullGCApproach,當該方法返回GCNotificationStatus指明垃圾回收已經進入等待執行的隊列,那麼你就可以把請求轉向到其他的伺服器,然後手執行一次手動垃圾回收(見下節)。然後,你調用GC.WaitForFullGCComplete方法,當該方法返回時,GC完成;那麼該伺服器就可以開始再次接收請求。然後在有需要的時候,你可以再次執行上述整個過程。
強制垃圾回收
通過調用GC.Collect方法,你可以隨時手動強制執行一次垃圾回收。調用GC.Collect沒有提供任何參數會執行一次完全垃圾回收。如果你提供一個整數類型的參數,那麼執行對應的垃圾回收。比如GC.Collect(0)執行Gen0垃圾回收。
// Forces a collection of all generations from 0 through Generation. // public static void Collect(int generation) { Collect(generation, GCCollectionMode.Default) } // Garbage Collect all generations. // [System.Security.SecuritySafeCritical] // auto-generated public static void Collect() { //-1 says to GC all generations. _Collect(-1, (int)InternalGCCollectionMode.Blocking); }
一般地,允許GC去決定何時執行垃圾回收可以得到最好的性能;這是因為強制垃圾回收會把Gen0的對象不必要地推送到Gen1(Gen1不必要地推送到Gen2),從而影響性能。這還會擾亂GC自身的調優能力--在程式運行時,GC動態地調整每種垃圾回收的臨界值以最大限度地提高性能。
但是,也有另外。最常見的可以執行手動垃圾回收的場景就是當一個應用程式進入休眠狀態,比如執行日常工作的windows服務。這樣的程式可能使用了System.Timters.Timer以每隔24小時觸發一次行為。當該行為完成之後,在接著的24小時之內沒有任何代碼會執行,那就意味著,在這段時間內,不會分配任何記憶體,因此GC就沒有機會被激活。服務在執行時所消耗的任何記憶體,在接著的24小時都會被持續占用--甚至是空對象圖。那麼解決方法就是在日常的行為完成之後調用GC.Collect()方法進行垃圾回收。
為了回收由於finalizer延遲回收的對象,你可以添加一行額外的代碼以調用WaitForPendingFinalizers,然後再調用一次垃圾回收
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
另外一種調用GC.Collect方法的場景是當你測試一個有Finazlier的類時。
記憶體壓力
.NET運行時基於一些列因素決定何時啟動垃圾回收,其中一個因素就是機器記憶體的總負載。 如果程式使用了非托管記憶體,那麼運行時會對其記憶體的使用情況持盲目地樂觀的態度,這是因為CLR之關心托管記憶體。通過告訴CLR已經分配了特定量的非托管記憶體記憶體,來減輕CLR的盲目性;調用CG.AddMemoryPresure方法可以完成該目的。如果取消該行為(當所占用的托管記憶體已經被釋放),那麼可以調用GC.RemoveMemoryPressure。
管理記憶體泄漏
在非托管語言中,比如C++,你必須記住當對象不再使用時,應手動地釋放記憶體;否則,將導致記憶體泄漏。在托管世界中,記憶體泄漏這種錯誤時不可能發生的,這歸功於CLR的自動垃圾回收。
儘管如此,大型的和複雜的.NET程式也會出現記憶體泄漏;只不錯記憶體泄漏的方式比較溫和,但具有相同的癥狀和結果:在程式的生命周期內,它消耗越來越多的記憶體,到最後導致程式重啟。好消息是,托管記憶體泄漏通常容易診斷和預防。
托管記憶體泄漏是由不再使用的活對象引起,這些對象之所以存活是憑藉不再使用引用或者被遺忘的引用。一種常見的例子就是事件處理器--它們堆目標對象保存了一個引用(除非目標是靜態方法)。比如,下麵的類:
class Host { public event EventHandler Click; } class Client { Host _host; public Client (Host host) { _host = host; _host.Click += HostClicked; } void HostClicked (object sender, EventArgs e) { ... } }
下麵的測試類包含1個方法實例化了1000個Client對象
class Test { static Host _host = new Host(); public static void CreateClients() { Client[] clients = Enumerable.Range (0, 1000) .Select (i => new Client (_host)) .ToArray(); // Do something with clients ... } }
你可能會認為,當CeateClients方法結束後,這個1000個Client對象理解適用於垃圾回收。很不幸,每個Client對象都包含一個引用:_host對象,並且該對象的Click事件引用每個Client實例。 如果Click事件不觸發,那麼就不會引起註意,或者HostClicked方法不做任何事情也不會引起註意。
解決這個問題的一種方式就是使Client類實現介面IDisposable,並且在dispose方法中,移除時間處理器
public void Dispose() { _host.Click -= HostClicked; }
Client實例的使用者,在使用完實例之後,調用Client類的dispose方法處置該實例
Array.ForEach (clients, c => c.Dispose());
下麵的對比展示兩種方式的差別
CLR Profiler Index |
實現IDisposable | 未實現IDisposable |
Time line | ||
Heap statistics | ||
GC Generatation Sizes |
計時器
不要忘記timmers也會引起記憶體泄漏。根據計時器的種類,會引發兩種不同的記憶體泄漏。首先我們來看System.Timers命名空間下的計時器。在下麵的例子中,Foo類每秒調用一次tmr_Elapsed方法
using System.Timers; class Foo { Timer _timer; Foo() { _timer = new System.Timers.Timer { Interval = 1000 }; _timer.Elapsed += tmr_Elapsed; _timer.Start(); } void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... } }
很不幸,Foo的實例決定不會被回收。原因在於.NET Framework本身持有對計活動的時器的引用,從而導致.net framework會觸發這些計時器的Elapsed事件。因此
- .NET Framework將使_timer處於活動狀態
- 通過tmr_Elapsed事件處理器,_timer將使Foo實現處於活動狀態
當你意識到Timer實現了IDisposable介面之後,解決的方法就在也明顯不過了。處置Timer實例以停止計時器,並確保.NET Framework不再引用該計時器對象。
class Foo : IDisposable { ... public void Dispose() { _timer.Dispose(); } }
相對於我們上面討論的內容,WPF和Windows窗體的計時器表現出完全相同的方式。
然而,System.Threading命名空間下的計時器確是一個特例。.NET Framework沒有引用活動線程計時器;想法,卻直接引用回調代理。這就意味著如果你忘記處置線程計時器,那麼finalizer會自動觸發並停止計時器然後處置該計時器。比如:
static void Main() { var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000); GC.Collect(); System.Threading.Thread.Sleep (10000); // Wait 10 seconds } static void TimerTick (object notUsed) { Console.WriteLine ("tick"); }
如果上面的代碼編譯為發佈模式,那麼計時器會被回收,並且在它再次觸發之前被處置(finalized)。同樣地,我們可以在計時器結束後通過處置該計數器以修複這個問題
using (var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000)) { GC.Collect(); System.Threading.Thread.Sleep (10000); // Wait 10 seconds }
using語句會隱式地調用tmr.Dispose方法,以確保tmr變數確實處於“使用(活動狀態)”;因此不會在代碼塊結束之前被當作是死對象。諷刺的是,調用Dispose方法實際上使對象存活的時間更長了。
診斷記憶體泄漏
避免托管記憶體泄漏的最簡單方式就是在編寫應用程式時就添加監控記憶體占用。你可以在程式中通過調用下麵的代碼來獲取當前記憶體的使用情況
long memoryUsed = GC.GetTotalMemory (true);
如果你採取測試驅動開發,那麼你可以使用單元測試判斷是否按照期望釋放了記憶體。入股這樣的判斷失敗,那麼接著你就應該檢查你最近對程式所作的修改。
如果你已經有一個大型程式,並且該程式存在托管記憶體泄漏問題,那麼你應該使用windgb.exe工具來幫助你解決問題。當然你還可以使用其他的圖形化工具,比如CLR Profiler, SciTech的Memory Profiler,或者Red Gate的ANTS Memory Profiler。
弱引用
有時候,引用一個對GC而言是“隱形”的對象,並且對象保持活動狀態,這非常有用。這既是弱引用,它由System.WeakReference類實現。使用WeakReference,使用其構造器函數並傳入目標對象。
var sb = new StringBuilder ("this is a test"); var weak = new WeakReference (sb); Console.WriteLine (weak.Target); // This is a test
如果目標對象僅僅由一個或多個弱引用所引用,那麼GC會把其加入到垃圾回收隊列中。如果目的對象被回收,那麼WeakReference的Target屬相則為NULL。