在.net 編程環境中,系統的資源分爲託管資源和非託管資源。
對於託管的資源的回收工做,是不須要人工干預回收的,並且你也沒法干預他們的回收,所可以作的只是瞭解.net CLR如何作這些操做。也就是說對於您的應用程序建立的大多數對象,能夠依靠 .NET Framework 的垃圾回收器隱式地執行全部必要的內存管理任務。
對於非託管資源,您在應用程序中使用完這些非託管資源以後,必須顯示的釋放他們,例如System.IO.StreamReader的一個文件對象,必須顯示的調用對象的Close()方法關閉它,不然會佔用系統的內存和資源,並且可能會出現意想不到的錯誤。
我想說到這裏,必定要清楚什麼是託管資源,什麼是非託管資源了?
最多見的一類非託管資源就是包裝操做系統資源的對象,例如文件,窗口或網絡鏈接,對於這類資源雖然垃圾回收器能夠跟蹤封裝非託管資源的對象的生存期,但它不瞭解具體如何清理這些資源。還好.net Framework提供了Finalize()方法,它容許在垃圾回收器回收該類資源時,適當的清理非託管資源。若是在MSDN Library 中搜索Finalize將會發現不少相似的主題,這裏列舉幾種常見的非託管資源:ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等等資源。可能在使用的時候不少都沒有注意到!
關於託管資源,就不用說了撒,像簡單的int,string,float,DateTime等等,.net中超過80%的資源都是託管資源。
非託管資源如何釋放,.NET Framework 提供 Object.Finalize 方法,它容許對象在垃圾回收器回收該對象使用的內存時適當清理其非託管資源。默認狀況下,Finalize 方法不執行任何操做。默認狀況下,Finalize 方法不執行任何操做。若是您要讓垃圾回收器在回收對象的內存以前對對象執行清理操做,您必須在類中重寫 Finalize 方法。然而你們均可以發如今實際的編程中根本沒法override方法Finalize(),在C#中,能夠經過析構函數自動生成 Finalize 方法和對基類的 Finalize 方法的調用。
例如:
~MyClass()
{
// Perform some cleanup operations here.
}
該代碼隱式翻譯爲下面的代碼。
protected override void Finalize()
{
try
{
// Perform some cleanup operations here.
}
finally
{
base.Finalize();
}
}
可是,在編程中,並不建議進行override方法Finalize(),由於,實現 Finalize 方法或析構函數對性能可能會有負面影響。一個簡單的理由以下:用 Finalize 方法回收對象使用的內存須要至少兩次垃圾回收,當垃圾回收器回收時,它只回收沒有終結器(Finalize方法)的不可訪問的內存,這時他不能回收具備終結器(Finalize方法)的不能夠訪問的內存。它改成將這些對象的項從終止隊列中移除並將他們放置在標記爲「準備終止」的對象列表中,該列表中的項指向託管堆中準備被調用其終止代碼的對象,下次垃圾回收器進行回收時,就回收並釋放了這些內存。
C#託管及未託管對象管理
c#中的對象分爲值類型和引用類型,兩者最大的區別在於數據的存儲方式和存儲位置.WINDOWS操做系統使用虛擬尋址系統來管理程序運行時產生的數據存放.簡單的說,該系統管理着一個內存區域,在該區域中劃撥出一部分出來專門存放值類型變量,稱爲堆棧,堆棧採用先進後出的原則,將值類型變量從區域的最高地址位開始向低位地址存儲,先進後出,後進先出的管理方式保證了值類型變量在出了做用域後能即便的清除佔用的內存區域,因爲堆棧速度快,所保存的數據通常不太大,這部分通常不須要用戶專門操做. 值類型保存在堆棧彙總, 堆棧有很是高的性能,但對於全部的變量來講仍是不太靈活。一般咱們但願使用一個方法分配內存,來存儲一些數據,並在方法退出後的很長一段時間內數據還是可使用的。只要是用new運算符來請求存儲空間,就存在這種可能性——例如全部的引用類型。此時就要使用託管堆。它在垃圾收集器的控制下工做,託管堆(或簡稱爲堆)是系統管理的大內存區域中的另外一個內存區域。要了解堆的工做原理和如何爲引用數據類型分配內存,看看下面的代碼:
Customer arabel = new Customer();
這行代碼完成了如下操做:首先,分配堆上的內存,以存儲Customer實例(一個真正的實例,不僅是一個地址)。而後把變量arabel的值設置爲分配給新Customer對象的內存地址(它還調用合適的Customer()構造函數初始化類實例中的字段,但咱們沒必要擔憂這部分)。
Customer實例沒有放在堆棧中,而是放在內存的堆中。若是咱們這樣操做:
Customer newaddress = arabel ;
這時候,newaddress也會保存在堆棧中,其值和arabel 相同,都是存儲Customer實例的堆地址.
知道了這些,咱們會發現這樣一個問題,若是堆棧中arabel 和newaddress兩個變量過時銷燬,那堆中保存的Customer對象會怎樣?實際上它仍保留在堆中,一直到程序中止,或垃圾收集器刪除它爲止. C#的垃圾收集器若是沒有顯示調用,會定時運行並檢查內存,刪除沒有任何變量引用的數據.看起來彷佛不錯,可是想一想,垃圾回收器並非時時檢查,它是定時運行,而在這段時間內若是產生大量的過時數據駐留在內存中..... 那麼或許咱們能夠經過調用System.GC.Collect(),強迫垃圾收集器在代碼的某個地方運行,System.GC是一個表示垃圾收集器的.NET基類, Collect()方法則調用垃圾收集器。可是,這種方式適用的場合不多,(難道銷燬一個對象就讓垃圾回收檢查一便內存嗎?)例如,代碼中有大量的對象剛剛中止引用,就適合調用垃圾收集器。何況垃圾收集器的邏輯不能保證在一次垃圾收集過程當中,從堆中刪除全部過時數據,對於不受垃圾回收器管理的未託管對象(例如文件句柄、網絡鏈接和數據庫鏈接),它是無能爲力的。那該怎麼作呢?
這時須要制定專門的規則,確保未託管的資源在回收類的一個實例時釋放。
在定義一個類時,可使用兩種機制來自動釋放未託管的資源。這些機制經常放在一塊兒實現,由於每一個機制都爲問題提供了略爲不一樣的解決方法。這兩個機制是:
● 聲明一個析構函數,做爲類的一個成員
● 在類中實現System.IDisposable接口
下面依次討論這兩個機制,而後介紹如何同時實現它們,以得到最佳的效果。
析構函數
前面介紹了構造函數能夠指定必須在建立類的實例時進行的某些操做,在垃圾收集器刪除對象時,也能夠調用析構函數。因爲執行這個操做,因此析構函數初看起來彷佛是放置釋放未託管資源、執行通常清理操做的代碼的最佳地方。可是,事情並非如此簡單。因爲垃圾回收器的運行規則決定了,不能在析構函數中放置須要在某一時刻運行的代碼,若是對象佔用了寶貴而重要的資源,應儘量快地釋放這些資源,此時就不能等待垃圾收集器來釋放了.
IDisposable接口
一個推薦替代析構函數的方式是使用System.IDisposable接口。IDisposable接口定義了一個模式(具備語言級的支持),爲釋放未託管的資源提供了肯定的機制,並避免產生析構函數固有的與垃圾函數器相關的問題。IDisposable接口聲明瞭一個方法Dispose(),它不帶參數,返回void,Myclass的方法Dispose()的執行代碼以下:
class Myclass : IDisposable
{
public void Dispose()
{
// implementation
}
}
Dispose()的執行代碼顯式釋放由對象直接使用的全部未託管資源,並在全部實現IDisposable接口的封裝對象上調用Dispose()。這樣,Dispose()方法在釋放未託管資源時提供了精確的控制。
假定有一個類ResourceGobbler,它使用某些外部資源,且執行IDisposable接口。若是要實例化這個類的實例,使用它,而後釋放它,就可使用下面的代碼:
ResourceGobbler theInstance = new ResourceGobbler();
// 這裏是theInstance 對象的使用過程
theInstance.Dispose();
若是在處理過程當中出現異常,這段代碼就沒有釋放theInstance使用的資源,因此應使用try塊,編寫下面的代碼:
ResourceGobbler theInstance = null;
try
{
theInstance = new ResourceGobbler();
// 這裏是theInstance 對象的使用過程
}
finally
{
if (theInstance != null) theInstance.Dispose();
}
即便在處理過程當中出現了異常,這個版本也能夠確保老是在theInstance上調用Dispose(),老是釋放由theInstance使用的資源。可是,若是老是要重複這樣的結構,代碼就很容易被混淆。C#提供了一種語法,能夠確保在引用超出做用域時,在對象上自動調用Dispose()(但不是Close())。該語法使用了using關鍵字來完成這一工做—— 但目前,在徹底不一樣的環境下,它與命名空間沒有關係。下面的代碼生成與try塊相對應的IL代碼:
using (ResourceGobbler theInstance = new ResourceGobbler())
{
// 這裏是theInstance 對象的使用過程
}
using語句的後面是一對圓括號,其中是引用變量的聲明和實例化,該語句使變量放在隨附的複合語句中。另外,在變量超出做用域時,即便出現異常,也會自動調用其Dispose()方法。若是已經使用try塊來捕獲其餘異常,就會比較清晰,若是避免使用using語句,僅在已有的try塊的finally子句中調用Dispose(),還能夠避免進行額外的縮進。
注意:
對於某些類來講,使用Close()要比Dispose()更富有邏輯性,例如,在處理文件或數據庫鏈接時,就是這樣。在這些狀況下,經常實現IDisposable接口,再執行一個獨立的Close()方法,來調用Dispose()。這種方法在類的使用上比較清晰,還支持C#提供的using語句。
前面的章節討論了類所使用的釋放未託管資源的兩種方式:
● 利用運行庫強制執行的析構函數,但析構函數的執行是不肯定的,並且,因爲垃圾收集器的工做方式,它會給運行庫增長不可接受的系統開銷。
● IDisposable接口提供了一種機制,容許類的用戶控制釋放資源的時間,但須要確保執行Dispose()。
通常狀況下,最好的方法是執行這兩種機制,得到這兩種機制的優勢,克服其缺點。假定大多數程序員都能正確調用Dispose(),實現IDisposable接口,同時把析構函數做爲一種安全的機制,以防沒有調用Dispose()。下面是一個雙重實現的例子:
public class ResourceHolder : IDisposable
{
private bool isDispose = false;
// 顯示調用的Dispose方法
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 實際的清除方法
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
// 這裏執行清除託管對象的操做.
}
// 這裏執行清除非託管對象的操做
}
isDisposed=true;
}
// 析構函數
~ResourceHolder()
{
Dispose (false);
}
}
能夠看出,Dispose()有第二個protected重載方法,它帶一個bool參數,這是真正完成清理工做的方法。Dispose(bool)由析構函數和IDisposable.Dispose()調用。這個方式的重點是確保全部的清理代碼都放在一個地方。
傳遞給Dispose(bool)的參數表示Dispose(bool)是由析構函數調用,仍是由IDisposable.Dispose()調用——Dispose(bool)不該從代碼的其餘地方調用,其緣由是:
● 若是客戶調用IDisposable.Dispose(),該客戶就指定應清理全部與該對象相關的資源,包括託管和非託管的資源。
● 若是調用了析構函數,在原則上,全部的資源仍須要清理。可是在這種狀況下,析構函數必須由垃圾收集器調用,並且不該訪問其餘託管的對象,由於咱們再也不能肯定它們的狀態了。在這種狀況下,最好清理已知的未託管資源,但願引用的託管對象還有析構函數,執行本身的清理過程。
isDispose成員變量表示對象是否已被刪除,並容許確保很少次刪除成員變量。這個簡單的方法不是線程安全的,須要調用者確保在同一時刻只有一個線程調用方法。要求客戶進行同步是一個合理的假定,在整個.NET類庫中反覆使用了這個假定(例如在集合類中)。最後,IDisposable.Dispose()包含一個對System.GC. SuppressFinalize()方法的調用。SuppressFinalize()方法則告訴垃圾收集器有一個類再也不須要調用其析構函數了。由於Dispose()已經完成了全部須要的清理工做,因此析構函數不須要作任何工做。調用SuppressFinalize()就意味着垃圾收集器認爲這個對象根本沒有析構函數.
正確理解以上內容,能夠大大優化系統性能,及時釋放不須要的數據,不能僅靠C#提供的自動回收機制,也須要程序員使用更靈活的辦法!兩者合一既能讓程序運行飛快,也讓系統更加穩定!
託管程序中資源的釋放問題
.Net所指的託管只是針對內存這一個方面,並非對於全部的資源;所以對於Stream,數據庫的鏈接,GDI+的相關對象,還有Com對象等等,這些資源並非受到.Net管理而統稱爲非託管資源。而對於內存的釋放和回收,系統提供了GC-Garbage Collector,而至於其餘資源則須要手動進行釋放。
那麼第二個概念就是什麼是垃圾,經過我之前的文章,會了解到.Net類型分爲兩大類,一個就是值類型,另外一個就是引用類型。前者是分配在棧上,並不須要GC回收;後者是分配在堆上,所以它的內存釋放和回收須要經過GC來完成。GC的全稱爲「Garbage Collector」,顧名思義就是垃圾回收器,那麼只有被稱爲垃圾的對象才能被GC回收。也就是說,一個引用類型對象所佔用的內存須要被GC回收,須要先成爲垃圾。那麼.Net如何斷定一個引用類型對象是垃圾呢,.Net的判斷很簡單,只要斷定此對象或者其包含的子對象沒有任何引用是有效的,那麼系統就認爲它是垃圾。
明確了這兩個基本概念,接下來講說GC的運做方式以及其的功能。內存的釋放和回收須要伴隨着程序的運行,所以系統爲GC安排了獨立的線程。那麼GC的工做大體是,查詢內存中對象是否成爲垃圾,而後對垃圾進行釋放和回收。那麼對於GC對於內存回收採起了必定的優先算法進行輪循回收內存資源。其次,對於內存中的垃圾分爲兩種,一種是須要調用對象的析構函數,另外一種是不須要調用的。GC對於前者的回收須要經過兩步完成,第一步是調用對象的析構函數,第二步是回收內存,可是要注意這兩步不是在GC一次輪循完成,即須要兩次輪循;相對於後者,則只是回收內存而已。
很明顯得知,對於某個具體的資源,沒法確切知道,對象析構函數何時被調用,以及GC何時會去釋放和回收它所佔用的內存。那麼對於從C、C++之類語言轉換過來的程序員來講,這裏須要轉變觀念。 那麼對於程序資源來講,咱們應該作些什麼,以及如何去作,才能使程序效率最高,同時佔用資源能儘快的釋放。前面也說了,資源分爲兩種,託管的內存資源,這是不須要咱們操心的,系統已經爲咱們進行管理了;那麼對於非託管的資源,這裏再重申一下,就是Stream,數據庫的鏈接,GDI+的相關對象,還有Com對象等等這些資源,須要咱們手動去釋放。
如何去釋放,應該把這些操做放到哪裏比較好呢。.Net提供了三種方法,也是最多見的三種,大體以下:
<!--[if !supportLists]-->1. <!--[endif]-->析構函數;
<!--[if !supportLists]-->2. <!--[endif]-->繼承IDisposable接口,實現Dispose方法;
<!--[if !supportLists]-->3. <!--[endif]-->提供Close方法。
通過前面的介紹,能夠知道析構函數只能被GC來調用的,那麼沒法肯定它何時被調用,所以用它做爲資源的釋放並非很合理,由於資源釋放不及時;可是爲了防止資源泄漏,畢竟它會被GC調用,所以析構函數能夠做爲一個補救方法。而Close與Dispose這兩種方法的區別在於,調用完了對象的Close方法後,此對象有可能被從新進行使用;而Dispose方法來講,此對象所佔有的資源須要被標記爲無用了,也就是此對象被銷燬了,不能再被使用。例如,常見SqlConnection這個類,當調用完Close方法後,能夠經過Open從新打開數據庫鏈接,當完全不用這個對象了就能夠調用Dispose方法來標記此對象無用,等待GC回收。明白了這兩種方法的意思後,你們在往本身的類中添加的接口時候,不要歪曲了這二者意思。
接下來講說這三個函數的調用時機,我用幾個試驗結果來進行說明,可能會使你們的印象更深。
首先是這三種方法的實現,大體以下:
public class DisposeClass:IDisposable
{
public void Close()
{
Debug.WriteLine( "Close called!" );
}
~DisposeClass()
{
Debug.WriteLine( "Destructor called!" );
}
#region IDisposable Members
public void Dispose()
{
// TODO: Add DisposeClass.Dispose implementation
Debug.WriteLine( "Dispose called!" );
}
#endregion
}
對於Close來講不屬於真正意義上的釋放,除了注意它須要顯示被調用外,我在此對它很少說了。
而對於析構函數而言,不是在對象離開做用域後馬上被執行,只有在關閉進程或者調用方法的時候才被調用,參看以下的代碼運行結果。
private void Create()
{
DisposeClass myClass = new DisposeClass();
}
private void CallGC()
{
GC.Collect();
}
// Show destructor
Create();
Debug.WriteLine( "After created!" );
CallGC();
運行的結果爲:
After created!
Destructor called!
顯然在出了Create函數外,myClass對象的析構函數沒有被馬上調用,而是等顯示調用GC.Collect才被調用。
對於Dispose來講,也須要顯示的調用,可是對於繼承了IDisposable的類型對象可使用using這個關鍵字,這樣對象的Dispose方法在出了using範圍後會被自動調用。例如:
using( DisposeClass myClass = new DisposeClass() )
{
//other operation here
}
如上運行的結果以下:
Dispose called!程序員
-----------------------------算法
注:GC爲了提升回收的效率使用了Generation的概念,原理是這樣的,第一次回收以前建立的對象屬於Generation 0,以後,每次回收時這個Generation的號碼就會向後挪一,也就是說,第二次回收時原來的Generation 0變成了Generation 1,而在第一次回收後和第二次回收前建立的對象將屬於Generation 0。GC會先試着在屬於Generation 0的對象中回收,由於這些是最新的,因此最有可能會被回收,好比一些函數中的局部變量在退出函數時就沒有引用了(可被回收)。若是在Generation 0中回收了足夠的內存,那麼GC就不會再接着回收了,若是回收的還不夠,那麼GC就試着在Generation1中回收內存,如此往復。數據庫