每一個應用程序都要使用這樣或者那樣的資源,好比文件、內存緩衝區、屏幕空間、網絡鏈接、數據庫資源等。事實上,在面向對象的環境中,每一個類型都表明可供程序使用的一種資源。
要使用這些資源,必須爲表明資源的類型分配內存。
訪問一個資源所需的具體步驟以下:
#1,調用IL指令newobj, 爲表明資源的類型分配內存。C#中使用new操做符,編譯器就會自動生成該指令。
#2,初始化內存,設置資源的初始狀態,使資源可用。類型的實例構造器負責設置該初始狀態。
#3,訪問類型的成員(可根據須要反覆)來使用資源。
#4,摧毀資源的狀態以進行清理。
#5,釋放內存。垃圾回收將獨自負責這一步。
須要注意的是,值類型(含全部枚舉類型)、集合類型、String、Attribute、Delegate和Exception 所表明的資源無需執行特殊的清理操做。如,只要銷燬對象的內存中維護的字符數組,一個String資源就會被徹底清理。
CLR要求全部的資源都從託管堆(managed heap)分配。應用程序不須要的對象會被自動清除。那麼「託管堆又是如何知道應用程序再也不用一個對象?」
進程初始化時,CLR要保留一塊連續的地址空間,這個地址空間最初並無對象的物理內存空間。這個地址空間就是託管堆。託管堆還維護着一個指針,我把它稱爲NextObjPtr。指向下一個對象在堆中的分配位置。剛開始時候,NextObjPtr設爲保留地址空間的基地址。
IL指令newobj用於建立一個對象。許多語言都提供了一個new操做符,它致使編譯器在方法的IL代碼中生成一個newobj指令。newobj指令將致使CLR執行以下步驟:
#1,計算類型(極其全部基類型)的字段須要的字節數。
#2,加上字段的開銷所需的字節數。每一個對象都有兩個開銷字段:一個是類型對象指針,和一個同步塊索引。
#3,CLR檢查保留區域是否可以提供分配對象所需的字節數,若有必要就提交存儲(commit storage)。若是託管堆有足夠的可用空間,對象會被放入。對象是在NextObjPtr指針指向的地址放入的,而且爲它分配的字節會被清零。接着,調用類型的實例構造器(爲this參數傳遞NextObjPtr), IL指令newobj(或者C# new 操做符)將返回對象的地址。就在地址返回以前,NextObjPtr指針的值會加上對象佔據的字節數,這樣會獲得一個新值,它就指向下一個對象放入托管堆時的地址。
做爲對比,讓咱們看一下C語言運行時堆如何分配內存,它爲對象分配內存須要遍歷一個由數據結構組成的鏈表,一旦發現一個足夠大的塊,那個塊就會被拆分,同時修改鏈表節點中的指針,以確保鏈表的完整性。
對於託管堆,分配對象只需在一個指針上加一個值 - 這顯然要快得多。事實上,從託管堆中分配對象的速度幾乎能夠與從線程棧分配內存媲美!
另外,大多數堆(C運行時堆)都是在他們找到可用空間的地方分配對象。因此,若是連續建立幾個對象,這些對象極有可能被分散,中間相隔MB的地址空間。但在託管堆中,連續分配的對象能夠確保它們在內存中是連續的。
託管堆彷佛在實現的簡單性和速度方面遠遠優於普通的堆,如C運行時堆。而託管堆之因此有這些好處,是由於它作了一個至關大膽的假設 - 地址空間和存儲是無限的。而這個假設顯然是不成立的,也就是說託管堆必須經過某種機制來容許它作這樣的假設。這個機制就是垃圾回收器。
垃圾回收的工做原理
CLR的垃圾回收(garbage collection)。
應用程序調用new操做符建立對象時,可能沒有足夠的地址空間來分配對象。託管堆將對象須要的字節數加到NextObjPtr指針中的地址上來檢測這種狀況。若是結果值超過了地址空間的末尾,代表託管堆已滿,必須執行一次垃圾回收。
重要提示:
前面的描述有些過於簡單,事實上,垃圾回收是在第0代滿的時候發生的。有的垃圾回收器使用了代(generation)的機制,該機制惟一的目的就是提高性能。其基本思路,在應用程序的生存期中,新建的對象是新一代,而建立得比較早的是老一代。第0代就是最近分配的對象,從未被垃圾回收算法檢查過。在一次垃圾回收中,存活下來的對象被提高到另外一代(如第1代)。將對象劃分爲代,使垃圾回收器能專一於回收特定的代,而不是每次都要回收託管堆中的對象。這裏假設垃圾回收是在堆滿的時候發生的。
垃圾回收器檢查託管堆中是否有應用程序再也不使用的任何對象。若是有,它們使用的內存就能夠回收(如果垃圾回收以後,堆中仍然沒有可用的內存,new操做符將會拋出一個OutOfMemoryException)。垃圾回收器如何知道應用程序正在使用一個對象?這的確不是一個三言兩語就能夠說清楚的問題。
每一個應用程序都包含一組根(root)。每一個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要麼引用託管堆中的一個對象,要麼爲null。
如,類型中定義的任何靜態字段被認爲是一個根。除此以外,任何方法參數或局部變量也被認爲是一個根。只有引用類型的變量才被認爲是根;值類型的變量永遠不被認爲是根。
垃圾回收器會檢查寄存器中引用的對象都是根,而這些根引用的堆中的對象不該被視爲垃圾。除此以外,垃圾回收器還能夠沿着線程的調用棧上行,檢查每一個方法的內部表來肯定全部調用方法的根。最後,垃圾回收器遍歷全部類型對象來獲取靜態字段中存儲的根集合。
垃圾回收器開始執行時,它假設堆中全部對象都是垃圾。換句話說,它假設線程棧中沒有引用了堆中對象的變量,沒有CPU寄存器引用隊中的對象,也沒有靜態字段引用堆中的對象。
第一階段 - 標記
垃圾回收器的第一個階段就是所謂的
標記(marking)階段。在這個階段,垃圾回收器沿着線程棧上行以檢查全部根。若是發現一個根引用了一個對象,就在對象的「同步塊索引字段」上開啓一位 (即設置一個bit,或者說設置爲1)--對象就是這樣「標記」的。若標記某一個對象時,發現這個對象(如D對象)含有一個引用了另外一個對象(如H對象)的字段,會形成這個H對象也被標記。垃圾回收器就是如此,以遞歸的方式遍歷全部可達的對象。
標記好根和它的字段引用的對象以後,垃圾回收器檢查下一個根,並繼續標記對象。若是垃圾回收器試圖標記一個先前標記過的對象,就會中止沿着這個路徑走下去。這個行爲有兩個目的。一是,垃圾回收器不會屢次遍歷一組對象,因此性能獲得顯著提升;而是,若是存在對象的循環鏈表,能夠避免陷入無限循環。
檢查好全部根以後,堆中將包含一組已標記和未標記的對象。已標記的對象是經過應用程序的代碼可達的對象,而未標記的對象是不可達的。不可達的對象被認爲是垃圾,它們佔用的內存能夠回收。
第二階段 - 壓縮
如今,垃圾回收器開始第二個階段,即
壓縮(compact)階段,在這個階段中,垃圾回收器線性地遍歷堆,以尋找未標記(垃圾)對象的連續內存塊。(注意:此處「壓縮」並不是壓縮,即託管堆,增多可用內存;相反,這裏的壓縮更接近於「碎片整理」,事實上,正確意思即「變得更加緊湊」。這個事實上,源於上世紀的80年開始,人們將compact當作是compress的近義詞而翻譯成「壓縮」,以訛傳訛至今。)
若是發現的內存塊比較小,就忽略它們。可是,若是發現大的、可用的連續內存塊,垃圾回收器會把非垃圾的對象移動到這裏以壓縮堆。
很天然,移動內存中的對象以後,包含"指向這些對象的指針"的變量和CPU寄存器如今都會變得無效。因此,垃圾回收器必須從新訪問應用程序的全部根,並修改它們來指向對象的新內存位置。另外,若是對象中的字段指向的另外一個已經移動了位置的對象,垃圾回收器也要負責改正這些字段。堆內存壓縮以後,託管堆的NextObjPtr指針將指向緊接在最後一個非垃圾對象以後的位置。以下所示,一次垃圾回收後的託管堆:
如你所見,垃圾回收會形成顯著的性能損失,這是使用託管堆的主要特色。但要注意的是,垃圾回收只在第0代滿的時候纔會發生。在此以前,託管堆的性能遠遠高於C運行時堆。
最後,CLR的垃圾回收器提供了一些特殊的優化措施,能夠大幅度提升垃圾回收的性能。
到這裏,做爲一名程序員,你應該從前面的論述得出四點重要的認識:
第一點,沒必要本身實現代碼來管理應用程序所用的對象的生存期。垃圾回收機制使開發人員獲得解放,無需關注內存釋放,能夠專一真正要解決的問題
第二點,再也不發生對象泄露的狀況,由於任何對象只要沒有應用程序的根引用它,都會在某個時刻被垃圾回收器回收,因此應用程序將不可能再發生內存的泄露的狀況。
第三點,應用程序也再也不可能訪問一個被釋放的對象。由於,假如對象可達,就不會被釋放;假如不可達,應用程序就是沒得辦法訪問它。
第四點,由於垃圾回收致使了內存的壓縮(compact),因此託管對象不可能形成進程的虛擬地址空間的碎片化。若是是非託管堆,如C運行時堆,地址空間的碎片化現象可能很是嚴重。而後,一個例外是在使用大對象的時候,仍然是有可能碎片化的。
重要提示:在負責加載類型的那個AppDomain卸載以前,類型的靜態字段永遠是它引用的任何 對象的根,形成內存泄露的一個常見緣由就是讓某個靜態字段引用一個集合對象,而後不停地向集合對象添加數據項。靜態字段保持集合對象的存活,而集合對象保持它的全部數據項的存活。有鑑於此,應該儘可能避免使用靜態字段。
使用終結操做來釋放本地資源
大多數類型只須要內存就能夠正常工做,可是也有一些類型除了要使用內存,還要使用本地資源。
如,System.IO.FileStream類型須要打開一個文件(本地資源)並保存文件的句柄。而後,該類型的Read和Write方法用該句柄來操做文件。
終結(finalization)是CLR提供的一種機制,容許對象在垃圾回收器回收其內存
以前執行一些得體的清理工做。任何包裝了本地資源(如文件,網絡鏈接、套接字、互斥體或者其餘類型)的類型都必須支持終結操做。
簡單地說,類型實現了一個命名爲Finalize的方法。當垃圾回收器判斷一個對象是垃圾時,會調用對象的Finalize方法(若是有的話)。能夠這樣理解:實現了Finalize方法的任何類型其實是在說,它的全部對象都但願在「被處決以前吃上最後一頓餐」。
Microsoft C#團隊認爲,Finalize方法是在編程語言中須要特殊語法的一種方法(相似於C#要求用特殊的語法定義構造器)。所以,在C#中,必須在類名前加一個~符號來定義Finalize方法,以下所示:
C#定義的Finalize方法的特殊語法很是相似於C++定義析構器的語法。事實上,在C#編程語言規範的早起版本中,真的是將該方法稱爲析構器。可是,Finalize方法的工做原理和非託管C++的析構器徹底不一樣,這會使從一種語言遷移到另外一種語言的開發人員產生極大的混淆。
實現了Finalize方法時,通常都會調用Win32 CloseHandle函數,並向該函數傳遞本地資源的句柄。例如,FileStream類型定義了一個文件句柄字段,它標識了本地資源。
FileStream類型還定義了一個Finalize方法,它在內部調用CloseHandle函數,並向它傳遞文件句柄字段。這就確保了在託管的FileStream對象被肯定爲垃圾後,本地文件句柄會得以關閉。如果包裝了本地資源的類型沒有定義Finalize方法,本地資源就得不到關閉,致使資源泄露,直至進程終止。進程終止時,這些本地資源纔會被操做系統回收。
Finalize 方法是在垃圾回收器回收前調用,可是,CLR並不保證各個Finalize方法的調用順序。
如下五種事件會致使垃圾回收:
#1,第0代滿
第0代滿時,垃圾回收會自動開始。該事件是目前致使Finalize方法被調用的最多見的一種方式,由於隨着應用程序代碼運行並分配新對象,這個事件會天然而然地發生。
#2,
代碼顯式調用System.GC的靜態方法Collect
代碼能夠顯式請求CLR執行垃圾回收,雖然Microsoft強烈建議不要這樣作,但某些時候仍是有必要的。
#3,
Windows報告內存不足
CLR內部使用Win32的CreateMemoryResourceNotification和QueryMemoryResourceNotification 函數來監視系統的整體內存。若是Windows報告CLR內存不足,CLR將強制執行垃圾回收,即嘗試釋放已經死亡的對象,從而減少進程工做集的大小。
#4,
CLR卸載AppDomain
一個AppDomain被卸載時,CLR認爲該AppDomain中再也不存在任何根,所以會對全部代碼的對象執行垃圾回收。
#5,
CLR關閉
一個進程正常終止時(相對於從外部關閉,好比經過任務管理器關閉),CLR就會關閉。在關閉過程當中,CLR會認爲該進程中不存在任何根,所以會調用託管堆中的全部對象的Finalize方法。注意,CLR此時不會嘗試壓縮或釋放內存,由於整個進程都要終止,將由Windows負責回收進程的全部內存。
CLR使用一個特殊的,專用的線程來調用Finalize方法。對於前4種事件,若是一個Finalize方法進入了無限循環,這個特殊的線程會被阻塞(blocked),其餘Finalize方法將得不到調用。這種狀況很是糟糕,由於應用程序永遠都不能回收由可終結的對象佔據的內存 - 只要應用程序運行,就會一直泄露內存。
對於第5種事件,每一個Finalize方法有大約2秒鐘的時間返回。若是Finalize方法在2秒鐘內沒有返回,CLR將直接殺死(結束)該進程 - 不會調用更多的Finalize方法。另外,若是調用全部對象的Finalize方法的時間超過了40秒鐘(也許之後會改變這個值),CLR也會殺死進程。
補充介紹:
#1,若是AppDomain卸載,其AppDomain的IsFinalizingForUnload 方法將返回true。
#2,若是進程終止,System.Enviroment.HasShutdownStarted屬性將返回true。
終結操做的祕密
終結操做彷佛很簡單: 建立一個對象,當它被回收時,它的Finalize方法會獲得調用。而若深究,你會發現遠非這麼簡單。
應用程序建立一個新對象時候,new操做符會從堆中分配內存。若是對象的類型定義了Finalize方法,那麼在該類型的實例構造器被調用以前,會將指向該對象的一個指針放到一個終結列表(finalization list)中。
終結列表是由垃圾回收器控制的一個內部數據結構。列表中的每個項都指向一個對象 - 在回收該對象的內存以前,應該調用它的Finalize方法。
注意
System.Object定義了一個Finalize方法,雖然如此,可是CLR會忽略它。也就是說,構造一個類型的實例時,若是該類型的Finalize方法是從System.Object繼承的,就不認爲這個對象是「可終結」的。類型必須重寫Object的Finalize方法,這個類型及其派生類型的對象才被認爲是「可終結」的 。
如上圖,垃圾回收開始,對象B, E, G, H, I 和J被斷定爲垃圾。垃圾回收器掃描終結列表以查找指向這些對象的指針。找到一個指針後,該指針會從終結列表中移除,並追加到freachable隊列中。freachable隊列(發音是「F-reachable」)是垃圾回收器的另外一個內部數據結構。它中的每一個指針都表明其Finalize方法已經準備好調用的一個對象。垃圾回收完畢後託管堆的狀況:
能夠看出,對象B, G和H佔用的內存已經被回收,由於它們沒有Finalize方法。可是,對象E, I和J佔用的內存暫時不能回收,由於它們的Finalize方法尚未調用。
重要信息:
終結列表和freachable隊列之間的交互很是有意思。
首先,讓我告訴你freachable隊列這個名稱的由來。「f」明顯表明「終結」(finalization);freachable隊列中的每一個記錄項都是對託管堆中的一個對象的引用,該對象的Finalize方法應該被調用。「reachable」意味着對象是可達的。換言之,可將freachable隊列當作是像靜態字段那樣的一個根。所以,若是一個對象在freachable隊列中,它就是可達的,
不是垃圾。
簡單地說,當一個對象不可達,垃圾回收器就把它視爲垃圾。可是,當垃圾回收器將對象的引用從終結列表移至freachable隊列時,對象再也不被認爲是垃圾,其內存不能被回收。標記freachable對象時,這些對象的引用類型的字段也會被遞歸地標記;全部這些對象都會在垃圾回收過程當中存活下來。到這個時候,垃圾回收器才結束對垃圾的標識。因爲一些本來被認爲是垃圾的對象被從新認爲不是垃圾,因此從某種意義上說,這些對象「復活」了。而後,垃圾回收器開始壓縮(compact, 使得緊湊)可回收的內存,特殊的CLR線程清空freachable隊列,並執行每一個對象的Finalize方法。
垃圾回收器下一次調用時,會發現已經終結的對象稱爲真正的垃圾,由於應用程序的根再也不指向它,freachable隊列也再也不指向它。因此,這些對象的內存會直接回收。整個過程當中,注意可終結的對象須要執行兩次垃圾回收才能釋放它們佔用的內存。實際應用中,因爲對象可能被提高至另外一代,因此可能要求不止進行兩次垃圾回收(代的問題之後詳述)。以下圖展現了第二次垃圾回收後託管堆的狀況:
Dispose 模式:強制對象清理資源
Finalize方法很是有用,由於它確保了當託管對象的內存被釋放時,本地資源不會泄露。可是,Finalize方法問題在於,它的調用時間是不能保證的。另外,因爲它不是公共方法,因此類的用戶不能顯示調用它。
使用包裝了本地資源(好比文件、數據庫鏈接和位圖等)的託管類型時,肯定性地dispose或關閉對象的能力一般都是頗有用的。例如,你可能想打開一個數據庫鏈接,查詢一些記錄,而後關閉該數據庫鏈接 -- 在發生下一次垃圾回收以前,你不但願數據庫鏈接一直處於打開狀態,尤爲是下一次垃圾回收可能在你獲取了數據庫記錄的幾小時或者幾天以後纔會發生。
類型爲了提供肯定性dispose或者關閉對象的能力,要實現所謂的Dispose模式。類型爲了提供顯式進行資源清理的能力,必須遵照Dispose模式定義的規範。除此以外,若是一個類型實現了Dispose模式,使用該類型的開發人員就能夠準確地知道在對象不須要時,如何顯式地dispose它。
注意:
全部定義了Finalize方法的類型都應同時實現本節描述的Dispose模式,使類型的用戶對資源的生存期有更多的控制,可是,類型也可實現Dispose模式,但不定義Finalize方法。例如,System.IO.BinaryWriter就是這樣的類型。
System.IDispose的定義以下:
public

interface IDispose
{
void Dispose();
}
任何類型只要實現了該接口,就至關於聲稱本身遵循Dispose模式。簡單地說,這意味着類型提供了一個公共無參Dispose方法,可顯式調用它來釋放對象包裝的資源。注意,對象自己的內存不會從託管堆的內存中釋放,仍然要由垃圾回收器負責釋放對象的內存,並且具體時間不定。提供了Dispose模式的一些類型爲了方便起見,還提供了一個Close方法,而只是調用Dispose方法,但這對於Dispose模式來講並不是必須。如System.IO.FileStream類提供了Dispose模式,也提供了Close方法。然而,System.Threading.Timer類就沒有提供Close方法,雖然它也遵循Dispose模式。
實現了Dispose模式的類型
C# 的using語句
說明:
dispose,在英語語境中,它的意思是「擺脫」或者「除去」(get rid of)一個東西,尤爲是在這個東西很難除去的狀況下。
在.NET Framework 文檔中,它的官方翻譯是「釋放」,意思是顯式釋放或者清理對象包裝的資源。
之因此認爲「釋放」不恰當,除了和release一詞衝突以外,還由於dispose強調了「清理資源」,並且在完成(它包裝)資源的清理以後,對象自己的內存並不會釋放。
因此,「dispose一個對象」或者"close一個對象" 真正的意思是: 清理對象中包裝的資源(好比它的字段所引用的對象),而後等待垃圾回收器自動回收該對象自己所佔用的內存。