一. 託管資源的分配數組
CLR在運行時管理着一段內存地址空間(虛擬地址空間,在運行中會映射到物理內存地址中),分爲「託管堆」和「棧」兩部分,棧用於存儲值類型數據,它會在方法執行結束後自動銷燬其中引用的值類型變量,這一部分不屬於垃圾收集的範圍。託管堆用於引用類型的變量存儲,是垃圾收集的關鍵陣地。安全
託管堆是一段連續的地址空間,其中所分配出去的空間呈現出相似數組形態的隊列結構:多線程
NextObjPtr是託管堆所維護的一個內存指針,指示下一個對象分配的內存起始地址,它會隨着內存的分配而不斷移動(固然也會隨着內存垃圾回收而發生移動),永遠指向下一個空閒的地址。框架
到了這裏,咱們不妨與C++比較一下內存分配機制的效率(對效率不感興趣的大能夠跳過:)),順便讓C++的朋友們打消一些對CLR分配內存效率的疑慮。在查找空閒內存空間時,CLR只須要在NextObjPtr處直接留出指定大小的空間提供給數據初始化,而後計算新的空閒地址並重置NextObjPtr指針便可。而在C/C++中,在分配內存以前先要遍歷一遍內存佔用的鏈表以查找合適大小的內存塊,而後再修改此鏈表,這樣也很容易產生內存碎塊,使得內存分配性能降低。很明顯,.NET的分配方式效率更高。可是這種效率是以GC的勞動爲代價的。ide
二. 垃圾斷定函數
要進行垃圾收集,首先要知道什麼是垃圾。GC經過遍歷應用程序中的「根」來尋找垃圾。咱們能夠認爲根是一個指向引用類型對象內存地址的指針。若是一個對象沒有了根,就是它再也不被任何位置所引用,那麼它就是垃圾的候選者了。性能
值得注意的一點是,對象可能在其生存期結束以前就被列入垃圾名單,甚至已經被GC所暗殺!那是由於對象可能在生存期的某一時刻已經再也不被引用,若是在這個時候執行垃圾收集,那麼這個不幸的對象極有可能已經被列爲垃圾並被銷燬(爲何說是「可能」呢?由於它不必定在GC的視力範圍內。後面講到「代齡」時會詳細介紹相關細節)。this
1 publicstatic void Main()線程
2 {指針
3 string sGarbage= "I'm here";
4
5 //下面的代碼沒有再引用s,它已經成爲垃圾對象---固然,這樣的代碼自己也是垃圾;
6 //此時若是執行垃圾收集,則sGarbage可能已經魂歸西天
7
8 Console.WriteLine("Main() is end");
9 }
三. 對象代齡
儘管GC老是在默默爲咱們勞動,但它畢竟是由人創造的,人會偷懶,它也會。爲了減小每次的工做量,它老是但願可以減小工做的範圍;它堅信,越晚建立的對象每每越短命,所以它會集中精力處理這一部分的內存區域,暫且擱置其餘部分。GC引入「代齡」的概念來劃分對象生存級別。
CLR初始化後的第一批被建立的對象被列爲0代對象。CLR會爲0代對象設定一個容量限制,當建立的對象大小超過這個設定的容量上限時,GC就會開始工做,工做的範圍是0代對象所處的內存區域,而後開始搜尋垃圾對象,並釋放內存。當GC工做結束後,倖存的對象將被列爲第1代對象而保留在第1代對象的區域內。此後新建立的對象將被列爲新的一批0代對象,直到0代的內存區域再次被填滿,而後會針對0代對象區域進行新一輪的垃圾收集,以後這些0代對象又會列爲第1代對象,併入第1代區域內。第1代區域起初也會被設上一個容量限制值,等到第1代對象大小超過了這個限制以後,GC就會擴大戰場,對第1代區域也作一次垃圾收集,以後,又一次倖存下來的對象將會提高一個代齡,成爲第2代對象。
可見,有一些對象雖然符合垃圾的全部條件,但它們若是是第1代(甚至是第2代老臣)對象,而且第1代的分配量還小於被設定的限制值時,這些垃圾對象就不會被GC發現,而且能夠繼續存活下去。
另外,GC還會在工做過程當中汲取經驗,根據應用程序的特色而自動調整每代對象區域的容量,從而能夠更高效的工做。
應該瞭解的垃圾收集機制(二)
對於大多數應用而言,瞭解垃圾收集機制的主要動機並非爲了對內存「省吃儉用」,而是爲了處理非託管資源的控制問題,這些問題每每跟內存的大小沒有什麼關係。例如對一個文件進行操做,該什麼時候關閉文件,關閉文件時要注意什麼問題,若是忘了關閉會帶來什麼後果?這些都是咱們須要認真考慮的,不管你的內存有多大:)
對於這一類的操做,咱們不能依賴GC幫咱們作,由於它並不知道咱們在釋放時想幹什麼,它甚至不知道本身該幹什麼!咱們不得不本身動手來編寫處理代碼。固然,微軟已經爲咱們搭好了框架,就是這兩個函數:Finalize和Dispose。它們也表明了非託管清理的兩種方式:自動和手動。
一. Finalize
Finalize很像C++的析構函數,咱們在代碼中的實現形式爲這與C++的析構函數在形式上徹底同樣,但它的調用過程卻大不相同。
~ClassName() {//釋放你的非託管資源}
好比類A中實現了Finalize函數,在A的一個對象a被建立時(準確的說應該是構造函數被調用以前),它的指針被插入到一個finalization鏈表中;在GC運行時,它將查找finalization鏈表中的對象指針,若是此時a已是垃圾對象的話,它會被移入一個freachable隊列中,最後GC會調用一個高優先級線程,這個線程專門負責遍歷freachable隊列並調用隊列中全部對象的Finalize方法,至此,對象a中的非託管資源才獲得了釋放(固然前提是你正確實現了它的Finalize方法),而a所佔用的內存資源則必需等到下一次GC才能獲得釋放,因此一個實現了Finalize方法的對象必需等兩次GC才能被徹底釋放。
因爲Finalize是由GC負責調用,因此能夠說是一種自動的釋放方式。可是這裏面要注意兩個問題:第一,因爲沒法肯定GC什麼時候會運做,所以可能很長的一段時間裏對象的資源都沒有獲得釋放,這對於一些關鍵資源而言是很是要命的。第二,因爲負責調用Finalize的線程並不保證各個對象的Finalize的調用順序,這可能會帶來微妙的依賴性問題。若是你在對象a的Finalize中引用了對象b,而a和b二者都實現了Finalize,那麼若是b的Finalize先被調用的話,隨後在調用a的Finalize時就會出現問題,由於它引用了一個已經被釋放的資源。所以,在Finalize方法中應該儘可能避免引用其餘實現了Finalize方法的對象。
可見,這種「自動」釋放資源的方法並不能知足咱們的須要,由於咱們不能顯示的調用它(只能由GC調用),並且會產生依賴型問題。咱們須要更準確的控制資源的釋放。
二. Dispose
Dispose是提供給咱們顯示調用的方法。因爲對Dispose的實現很容易出現問題,因此在一些書籍上(如《Effective C#》和《Applied Microsoft.Net Framework Programming》)給出了一個特定的實現模式:
class DisposePattern :IDisposable
{
private System.IO.FileStream fs = new System.IO.FileStream("test.txt", System.IO.FileMode.Create);
~DisposePattern()
{
Dispose(false);
}
IDisposable Members#region IDisposable Members
public void Dispose()
{
//告訴GC不須要再調用Finalize方法,
//由於資源已經被顯示清理
GC.SupdivssFinalize(this);
Dispose(true);
}
#endregion
protected virtual void Dispose(bool disposing)
{
//因爲Dispose方法可能被多線程調用,
//因此加鎖以確保線程安全
lock (this)
{
if (disposing)
{
//說明對象的Finalize方法並無被執行,
//在這裏能夠安全的引用其餘實現了Finalize方法的對象
}
if (fs != null)
{
fs.Dispose();
fs = null; //標識資源已經清理,避免屢次釋放
}
}
}
}
在註釋中已經有了比較清楚的描述,另外還有一點須要說明:若是DisposePattern類是派生自基類B,而B是一個實現了Dispose的類,那麼DisposePattern中只須要override基類B的帶參的Dispose方法便可,而不須要重寫無參的Dispose和Finalize方法,此時Dispose的實現爲:
class DerivedClass : DisposePattern
{
protected override void Dispose(bool disposing)
{
lock (this)
{
try
{
//清理本身的非託管資源,
//實現模式與DisposePattern相同
}
finally
{
base.Dispose(disposing);
}
}
}
}
固然,若是DerivedClass自己沒有什麼資源須要清理,那麼就不須要重寫Dispose方法了,正如咱們平時作的一些對話框,雖然都是繼承於System.Windows.Forms.Form,但咱們經常不須要去重寫基類Form的Dispose方法,由於自己沒有什麼非託管的咚咚須要釋放。
瞭解GC的脾性在不少時候是很是必要的,起碼在出現資源泄漏問題的時候你不至於手足無措。我寫過一個生成excel報表的控件,其中對excel對象的釋放就讓我忙活了一陣。若是你作過excel開發的話,可能也遇到過結束excel進程之類的問題,特別是包裝成一個供別人調用的庫時,什麼時候釋放excel對象以確保進程結束是一個關鍵問題。固然,GC的內部機制很是複雜,還有許多內容可挖,但瞭解全部細節的成本過高,只需瞭解基礎,夠用就好。