有些對象需要顯示地銷毀代碼來釋放資源,比如打開的文件資源,鎖,操作系統句柄和非托管對象。在.NET中,這就是所謂的對象銷毀,它通過IDisposal接口來實現。不再使用的對象所占用的內存管理,必須在某個時候回收;這個被稱為無用單元收集的功能由CLR執行。
對象銷毀和垃圾回收的區別在於:對象銷毀通常是明確的策動;而垃圾回收完全是自動地。換句話說,程序員負責釋放文件句柄,鎖,以及操作系統資源;而CLR負責釋放內存。
本章將討論對象銷毀和垃圾回收,還描述了C#處理銷毀的一個備選方案--Finalizer及其模式。最後,我們討論垃圾回收器和其他內存管理選項的復雜性。
對象銷毀 垃圾回收 1)IDisposal接口
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#語言;這些規則的目的是定義一套便於使用的協議。這些協議如下:
這些規則同樣也適用於我們平常創建自定義類型,盡管它並不是強制性的。沒有誰能阻止你編寫一個不可銷毀的方法;然而,這麼做,你的同事也許會用高射炮攻擊你。
對於第三條規則,一個容器對象自動銷毀其子對象。最好的一個例子就是,windows容器對象比如Form對著Panel。一個容器對象可能包含多個子控件,那你也不需要顯示地銷毀每個字對象:關閉或銷毀父容器會自動關閉其子對象。另外一個例子就是如果你在DeflateStream包裝了FileStream,那麼銷毀DeflateStream時,FileStream也會被銷毀--除非你在構造器中指定了其他的指令。
Close和Stop
有一些類型除了Dispose方法之外,還定義了Close方法。Framework對於Close方法並沒有保持完全一致性,但在幾乎所有情況下,它可以:
對於後者一個典型的例子就是IDbConnecton類型,一個Closed的連接可以再次被打開;而一個Disposed的連接對象則不能。另外一個例子就是Windows程序使用ShowDialog的激活某個窗口對象:Close方法隱藏該窗口;而Dispose釋放窗口所使用的資源。
有一些類定義Stop方法(比如Timer或HttpListener)。與Dipose方法一樣,Stop方法可能會釋放非托管資源;但是與Dispose方法不同的是,它允許重新啟動。
銷毀對象應該遵循的規則是“如有疑問,就銷毀”。一個可以被銷毀的對象--如果它可以說話--那麼將會說這些內容:
“如果你結束對我的使用,那麼請讓我知道。如果只是簡單地拋棄我,我可能會影響其他實例對象、應用程序域、計算機、網絡、或者數據庫”
如果對象包裝了非托管資源句柄,那麼經常會要求銷毀,以釋放句柄。例子包括Windows Form控件、文件流或網絡流、網絡sockets,GDI+畫筆、GDI+刷子,和bitmaps。與之相反,如果一個類型是可銷毀的,那麼它會經常(但不總是)直接或間接地引用非托管句柄。這是由於非托管句柄對操作系統資源,網絡連接,以及數據庫鎖之外的世界提供了一個網關(出入口),這就意味著使用這些對象時,如果不正確的銷毀,那麼會對外面的世界代碼麻煩。
但是,遇到下面三種情形時,不要銷毀對象
第一種情況很少見。多數情形都可以在System.Drawing命名空間下找到:通過靜態成員或屬性獲取的GDI+對象(比如Brushed.Blue)就不能銷毀,這是因為該實現在程序的整個生命周期中都會用到。而通過構造器得到的對象實例,比如new SolidBrush,就應該銷毀,這同樣適用於通過靜態方法獲取的實例對象(比如Font.FromHdc)。
第二種情況就比較常見。下表以System.IO和System.Data命名空間下類型舉例說明
類型 銷毀功能 何時銷毀 MemoryStream 防止對I/O繼續操作 當你需要再次讀讀或寫流 StreamReader,第三者情況包含了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決定何時進行垃圾回收,它取決於許多因素,比如,剩余內存,已經分配的內存,上一次垃圾回收的時間。這就意味著,在一個對象被孤立後到期占用的內存被釋放之間,有一個不確定的時間延遲。該延遲的范圍可以從幾納秒到數天。
垃圾回收和內存占用// 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內部釋放,以及把這些資源讓給操作系統以供其他的進程使用。
根就是指保持對象依然處於活著的事物。如果一個對象不再直接或間接地被一個根引用,那麼該對象就適合於垃圾回收。
一個跟可以是:
正在執行的代碼可能涉及到一個已經刪除的對象,因此,如果一個實例方法正在執行,那麼該實例方法的對象必然按照上述方式被引用。
請注意,一組相互引用的對象的循環被視作無根的引用。換一種方式,也就是說,對象不能通過下面的箭頭指向(引用)而從根獲取,這也就是引用無效,因此這些對象也將被垃圾回收器處理。
class Test { ~Test() { // finalizer logic ... } }
(盡管與構造器的聲明相似,finalizer不能被聲明為public或static,也不能有參數,還不能調用其基類)
Finalizer是可能的,因為垃圾收集工作在不同的時間段。首先,垃圾回收識別沒有使用的對象以刪除該對象。這些待刪除的對象如果沒有Finalizer那麼就立即刪除。而那些擁有finalizer的對象會被保持存活並存在放到一個特殊的隊列中。
在這一點上,當你的程序在繼續執行的時候,垃圾收集也是完整的。而Finalizer線程卻在你程序運行時,自動啟動並在另外一個線程中並發執行,收集擁有Finalizer的對象到特殊隊列,然後執行它們的終止方法。在每個對象的finalizer方法執行之前,它依然非常活躍--排序行為視作一個跟對象。而一檔這些對象被移除隊列,並且這些對象的fainalizer方法已經執行,那麼這些對象就變成孤立的對象,會在下一階段的垃圾回收過程中被回收。
Finalizer非常有用,但它們也有一些限制:
總之,finalizer在一定程度上就好比律師--一旦有訴訟那麼你確實需要他們,一般你不想使用他們,除非萬不得已。如果你使用他們,那麼你需要100%確保你了解他們會為你做什麼。
下面是實施finalizer的一些准則:
一個流行的模式是使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的垃圾回收執行時間長,並且也不會經常發生。下圖展示了一個完全垃圾回收:
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的對象(這些對象由大對象組成)之後,馬上觸發垃圾回收。
大對象堆不適合於壓縮,這是因為發生垃圾回收時,移動內存大塊的代價非常高。如果這麼做,會帶來下面兩個後果:
大對象堆還是非通用的堆,大對象堆上的所有對象被視作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 Profilerusing 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事件。因此
當你意識到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。
var weak = new WeakReference(new StringBuilder("weak")) Console.WriteLine(weak.Target); // weak GC.Collect(); Console.WriteLine(weak.Target == null); // (true)
為了避免目標對象在測試其為null和使用目標對象之間被回收,把目標對象分配給一個局部變量
var weak = new WeakReference (new StringBuilder ("weak")); var sb = (StringBuilder) weak.Target; if (sb != null) { /* Do something with sb */ }
一旦目標對象分配給一個局部變量,那麼目的對象就有了一個強類型根對象,從而在局部變量使用期間不會被回收。
下面例子中的類通過弱引用追蹤所有被實例化的Widget對象,從而使這些實例不會被回收
class Widget { static List<WeakReference> _allWidgets = new List<WeakReference>(); public readonly string Name; public Widget (string name) { Name = name; _allWidgets.Add (new WeakReference (this)); } public static void ListAllWidgets() { foreach (WeakReference weak in _allWidgets) { Widget w = (Widget)weak.Target; if (w != null) Console.WriteLine (w.Name); } } }
這樣一個系統的唯一缺點就是,靜態列表會隨著時間推移而增加,逐漸累積對應null對象的弱引用。因此,你需要自己實現一些清理策略。
使用弱引用的目的之一是為了緩存大對象圖。通過弱引用,使得耗費內存的數據可以進行簡要的緩存而不是造成內存的大量占用。
_weakCache = new WeakReference (...); // _weakCache is a field ... var cache = _weakCache.Target; if (cache == null) { /* Re-create cache & assign it to _weakCache */ }
在實際上,該策略只會發揮一半的作用,這是因為你不能控制GC何時運行,並且也不能控制GC會會執行哪一類回收。尤其是,當你的緩存是在Gen0中,那麼這類內存會在微妙級別類被回收。因此,至少,你需要使用兩類緩存,通過它們,首先你擁有一個強類型,然後不時地把該強類型轉換成弱類型。
在前面的章節中,我們看到事件是如何引起內存洩漏。而且解決這種內存洩漏的最簡單方法是避免時間訂閱,或者對為訂閱事件的對象實現Dispose方法。此外,弱引用也提供了另外一種解決方案。
假設一個帶來對其目標持有一個弱引用。那麼這樣的一個代理並不會使其目標為活動狀態,除非這些目標對象有獨立的引用。當然,這並不會阻止一個被觸發的代理,在目標對象進入回收隊列之後但在GC開始對該目標對象執行回收前的時間段中,擊中一個未被引用的目標。為了該方法高效,你的代碼必須非常穩定。下面的代碼就是就是采用這種方式的具體實現:
public class WeakDelegate<TDelegate> where TDelegate : class { class MethodTarget { public readonly WeakReference Reference; public readonly MethodInfo Method; public MethodTarget (Delegate d) { Reference = new WeakReference (d.Target); Method = d.Method; } } List<MethodTarget> _targets = new List<MethodTarget>(); public WeakDelegate() { if (!typeof (TDelegate).IsSubclassOf (typeof (Delegate))) throw new InvalidOperationException ("TDelegate must be a delegate type"); } public void Combine (TDelegate target) { if (target == null) return; foreach (Delegate d in (target as Delegate).GetInvocationList()) _targets.Add (new MethodTarget (d)); } public void Remove (TDelegate target) { if (target == null) return; foreach (Delegate d in (target as Delegate).GetInvocationList()) { MethodTarget mt = _targets.Find (w => d.Target.Equals (w.Reference.Target) && d.Method.MethodHandle.Equals (w.Method.MethodHandle)); if (mt != null) _targets.Remove (mt); } } public TDelegate Target { get { var deadRefs = new List<MethodTarget>(); Delegate combinedTarget = null; foreach (MethodTarget mt in _targets.ToArray()) { WeakReference target = mt.Reference; if (target != null && target.IsAlive) { var newDelegate = Delegate.CreateDelegate ( typeof (TDelegate), mt.Reference.Target, mt.Method); combinedTarget = Delegate.Combine (combinedTarget, newDelegate); } else deadRefs.Add (mt); } foreach (MethodTarget mt in deadRefs) // Remove dead references _targets.Remove (mt); // from _targets. return combinedTarget as TDelegate; } set { _targets.Clear(); Combine (value); } } }
上述代碼演示了許多C#和CLR的有趣的地方。首先,我們在構造器中檢查了TDelegate是一個代理類型。這是因為C#本身的限制--因為下面的語句不符合C#的語法
... where TDelegate : Delegate // Compiler doesn't allow this
由於必須要進行類型限制,所以我們在構造器中執行運行時檢查。
在Combine方法和Remove方法中,我們執行了引用轉換,通過as運算符(而沒有使用更常見的轉換符)把target對象轉換成Delegate類型。這是由於C#不允許轉換符使用類型參數--因為它不能分清這是一個自定義的轉換還是一個引用抓換(下面的代碼不能拖過編譯)。
foreach(Delegate d in ((Delegate)target).GetInvocationList()) _targets.Add(new MethodTarget(d));
當調用GetInvocationList,由於這些方法可能被一個多播代理調用,多播代理就是一個代理有多余一個的方法接收。
對於Target屬性,我們使其為一個多播代理--通過一個弱引用包含所有的代理引用,從而使其目標對象保持活動。然後我們清楚剩余的死引用,這樣可以避免_targets列表無限制的增長。下面的代碼演示了如何使用我們上面創建的實現了事件的代理類:
public class Foo { WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>(); public event EventHandler Click { add { _click.Combine (value); } remove { _click.Remove (value); } } protected virtual void OnClick (EventArgs e) { EventHandler target = _click.Target; if (target != null) target (this, e); } }
請注意,在觸發事件時,在檢查和調用之前,我們把_click.Target對象賦值給一個臨時變量。這就避免了目標對象被GC回收的可能性。
參考
http://msdn.microsoft.com/en-US/library/system.idisposable.aspx