14 使用垃圾回收和資源管理

1.對象的生存期

首先我們來看一下創建對象的過程。對象用new操作符創建。下例創建Square(正方形)類的新實例

class Square
{
    …
    void Draw()
    {
        …
    }

}

Square mySquare = new Square();//Square是引用類型

new 表面上是單步操作,但實際分兩步走:

1.new操作符從堆中分配原始內存。這個階段無法進行任何干預

2.new操作符將原始內存轉換成對象;它必須初始化對象。可用構造器控制這一階段

 

創建好對象後,可用點操作符(.)訪問其成員。例如,Square 類提供了Draw方法:

mySquare.Draw();

mySquare變量離開作用域時,它引用的Square對象就沒人引用了,所以對象可被銷燬,佔用的內存可被回收(稍後會講到,這並不是馬上就發生的)。和對象的創建相似,對象的銷燬也分兩步走,過程剛好與創建相反。

 

1. CLR執行清理工作,可以寫一個析構器來加以控制。

2. CLR將對象佔用的內存歸還給堆,解除對象內存的分配。對這個階段你沒有控制權。銷燬對象並將內存歸還給堆的過程稱爲垃圾回收

 

1.1編寫析構器

使用析構器,可以在對象被垃圾回收時執行必要的清理。CLR能自動清理對象使用的任何託管資源,所以許多時候都不需要自己寫析構器。但如果託管資源很大(比如一個多維數組),就可考慮將對該資源的所有引用都設爲null, 使資源能被立即清理。另外,如果對象引用了非託管資源(無論直接還是間接),析構器就更有用了。

注意:間接的非託管資源其實很常見。文件流、網絡連接、數據庫連接和Windows操作系統管理的其他資源都是例子。所以,如果方法要打開一個文件,就應考慮添加析構器在對象被銷燬時關閉文件。但取決於類中的代碼的結構,或許有更好、更及時的辦法關閉文件,詳情參見稍後對using語句的討論。

和構造器相似,析構器也是一一個特殊方法,只是CLR會在對象的所有引用都消失之後調用它。析構器的語法是先寫一個~符號,再添加類名。例如,下面的類在構造器中打開文件進行讀取,在析構器中關閉文件(注意這只是例子,不建議總是像這樣打開和關閉文件):

class FileProcessor
{

    Filestream file = null;
    public FlleProcessor(string fileName)
    {
        this.file = File.OpenRead(1leName); //打開文件來讀取
    }

    ~FileProcessor()
    {
        this.file.Close(); //關閉文件
    }

}

 

析構器存在以下重要限制

(1)析構器只適合引用類型。值類型(例如struct)不能聲明析構器。

(2)不能爲析構器指定訪問修飾符(例如public).這是由於永遠不在自己的代碼中調用析構器一總 是由垃圾回收器(CLR的一部分)幫你調用。

(3)析構器不能獲取任何參數。這同樣是由於永遠不由你自己調用析構器。

 

編譯器內部自動將析構器轉換成對object.Finalize方法的一個重寫版本的調用。例如,編譯器將以下析構器:

class FileProcessor
{

    ~FileProcessor() { //你的代碼放到這裏}

}

轉換成以下形式:

class FileProcessor
{

    protected override void Finalize()
    {

        try { //你的代碼放在這裏}

        finally { base.Finalize(); }

    }

}

注意:只有編譯器才能進行這個轉換。你不能自己重寫Finalize,也不能自己調用Finalize.

 

1.2爲什麼要使用垃圾回收器

在C#中,你永遠不能親自銷燬對象。沒有任何語法支持該操作。相反,CLR在它認爲合適的時間幫你做這件事情。注意,可能存在對一一個對象的多個引用。在下例中,變量myFp

和referenceToMyFp引用同一個FileProcessor對象。

FlleProcessor myFp = new FileProcessor();

FileProcessor referenceToMyFp = myFp;

能創建對一個對象的多少個引用?答案是沒有限制。這對對象的生存期產生了影響。CLR必須跟蹤所有引用。如果變量myFp 不存在了(離開作用域),其他變量(比如referenceToMyFp)可能仍然存在,FileProcessor對象使用的資源還不能被回收(文件還不能被關閉)。因此,對象的生存期不能和特定的引用變量綁定。只有在對一個對象的所有引用都消失之後,纔可以銷燬該對象,回收其內存以進行重用。

 

可以看出,對象生存期管理是相當複雜的-件事情, 這正是C#的設計者決定禁止由你銷燬對象的原因。如果由程序員負責銷燬對象,遲早會遇到以下情況之一。

(1)忘記銷燬對象。這意味着對象的析構器(如果有的話)不會運行,清理工作不會進行,內存不會回收到堆。最終的結果是,內存很快被消耗完。

(2)試圖銷燬活動對象,造成一個或多個變量容納對已銷燬的對象的引用,即所謂的虛懸引用。虛懸引用要麼引用未使用的內存,要麼引用同一個內存位置的-一個完全不相干的對象。無論如何,使用虛懸引用的結果都是不確定的,甚至可能帶來安全風險。什麼都可能發生。

(3)試圖多次銷燬同一個對象。這可能是、也可能不是災難性的,具體取決於析構器中的代碼怎麼寫。

 

對於C#這種將健壯性和安全性擺在首要位置的語言,這些問題顯然是不能接受的。取而代之的是,必須由垃圾回收器負責銷燬對象。垃圾回收器能做出以下幾點擔保。

(1)每個對象都會 被銷燬,它的析構器會運行。程序終止時,所有未銷燬的對象都會被銷燬。

(2)每個對象只被銷燬一次。

(3)每個對象只有在它不可達時(不存在對該對象的任何引用)纔會被銷燬。

 

但要注意,垃圾回收不一定在對象不再需要之後立即進行。垃圾回收可能是一個代價較高的過程,所以“運行時”只有在覺得必要時才進行垃圾回收(例如,在它認爲可用內存不夠的時候,或者堆的大小超過系統定義閥值的時候)。

 

注意:可通過靜態方法System.GC.Collect 在程序中調用垃圾回收器。但除非萬不得已,否則不建議這樣做。System. GC.Collect方法將啓動垃圾回收器,但回收過程是異步發生的方法結束時,程序員仍然不知道對象是否已被銷燬。讓CLR決定垃圾回收的最佳時機!

 

1.3垃圾回收器的工作原理

垃圾回收器是非常複雜的軟件,能自行調整,並進行了大量優化以便在內存需求與應用程序性能之間取得良好平衡。內部算法和結構比較複雜(Microsoft自己也在不斷改進垃圾回收器的性能),但它採取的大體步驟如下。

 

(1)構造所有可達對象的 一個映射(map).爲此,它會反覆跟隨對象中的引用字段。垃圾回收器會非常小心地構造映射,確保循環引用(你引用我,我引用你)不會造成無限遞歸任何不在映射中的對象肯定不可達。

(2)檢查是否有任何不可達對象包含一個需要運行的析構器(運行析構器的過程稱爲“終結”)。需終結的任何不可達對象都放到一個稱爲freachable (發音是F-reachable)的特殊隊列中。

(3)回收剩下的不可達對 象(即不需要終結的對象)。爲此,它會在堆中向下面移動可達

的對象,對堆進行“碎片整理",釋放位於堆項部的內存。一個可達對象被移動之後,會更新對該對象的所有引用。

(5)然後, 允許其他線程恢復執行。

(6)在一個獨立的線程中,對需要終結的不可達對象(現在,這些對象在freachable隊列中了)執行終結操作。

 

1.4慎用析構器

寫包含析構器的類,會使代碼和垃圾回收過程變複雜。此外,還會影響程序的運行速度。如果程序不包含任何析構器,垃圾回收器就不需要將不可達對象放到freachable 隊列並對它們進行“終結”(也就是不需要運行析構器)。顯然,一件事情做和不做相比,不做會快一些。所以,除非確有必要,否則請儘量避免使用析構器。例如,可以改爲使用using語句,待會討論

 

寫析構器時要小心。尤其注意,如果在析構器中調用其他對象,那些對象的析構器可

能已被垃圾回收器調用。記住,“終結” (調用析構器的過程)的順序是得不到任何保障的。

所以,要確定析構器不相互依賴,或相互重疊(例如,不要讓兩個析構器釋放同一個資源)。

 

2.資源管理

有時在析構器中釋放資源並不明智。有的資源過於寶貴,用完後應馬上釋放,而不是等待垃圾回收器在將來某個不確定的時間釋放。內存、數據庫連接和文件句柄等稀缺資源應儘快釋放。這時唯一-的選擇就是親自釋放資源。這是通過自己寫的資源清理(disposal)方法來實現的。可顯式調用類的資源清理方法,從而控制釋放資源的時機。

 

2.1資源清理方法

實現了資源清理方法的一個例子是來自System. IO命名空間的TextReader類。該類提供了從順序輸入流中讀取字符的機制。TextReader 包含虛方法Close,它負責關閉流,這就是一個資源清理方法。StreamReader 類從流(例如- 個打開的文件)中讀取字符,StringReader類則從字符串中讀取字符。這兩個類均從TextReader類派生,都重寫了Close方法。下例使用StreamReader類從文件中讀取文本行並在屏幕上顯示:

TextReader reader = new StreanReader(filename);
string line;
while (line  = reader ,ReadLine()) != null)
{

    Console .WriteLine(1ine);

}

reader.Close();

但這個例子存在一個問題,即它不是異常安全的。如果對ReadLine(或WriteLine)的調用拋出異常,對Close的調用就不會發生。如果經常發生這種情況,最終會耗盡文件句柄資源,無法打開任何更多的文件。

 

2.2異常安全的資源清理

對上面的例子進行改進:

TextReader reader = new StreanReader(filename);

try
{

    string line;
    while ((line = reader. ReadLine()) != null)
    {

        Console.WriteLine(1ine);

    }

}
finally
{
    reader .Close();

}

像這樣使用finally塊是可行的,但由於它存在幾個缺點,所以並不是特別理想。

(1)要釋放多個資源, 局面很快就會變得難以控制(將獲得嵌套的try和finally塊)。

(2)有時可能需要修改代碼來適應這一慣用法(例如,可能需要修改資源引用的聲明順序,記住將引用初始化爲null,並記住查驗finally塊中的引用不爲null)。

(3)它不能創建解決方案的 -一個抽象。這意味着解決方案難以理解,必須在需要這個

功能的每個地方重複代碼。

(4)對資源的引用保留在 finally塊之後的作用域中。這意味着可能不小心使用一個已釋放的資源。

using語句就是爲了解決所有這些問題而設計的。

 

2.3using語句和IDisposable接口

using語句提供了一個脈絡清晰的機制來控制資源的生存期。可以創建一個對象,這個對象會在using語句塊結束時銷燬。

using語句的語法如下:

using ( type variable = initialization )
{

    statementBlock

}

下面是確保代碼總是在TextReader上調用Close的最佳方式:

using (TextReader reader = new StreanReader(filename))
{

    string line;
    while ((line=reader .ReadLine()) != null)
    {
        Console.Writeline(line);
    }

}

這個using語句完全等價於以下形式:

TextReader reader =new StreanReader(filename);
try
{

    string line;
    while ((line =reader.ReadLine()) !=null)
    {
        Console .WriteLine(line);
    }

finally
{
    if (reader != null)
    {

        ((IDisposable)reader).Dispose();

    }

}

注意:using語句引入了它自己的代碼塊,這個塊定義了一個作用城。也就是說,在語句塊的末尾,using 語句所聲明的變量會自動離開作用城,所以不可能因爲不小心而訪問已被清理的資源。

using語句聲明的變量的類型必須實現IDisposable 接口。IDisposable 接口在System命名空間中,只包含-一個 名爲Dispose的方法:

namespace System
{

interface Idisposable
{

    void Dispose();

}

}

Dispose方法的作用是清理對象使用的任何資源。StreamReader 類正好實現了IDisposable接口,它的Dispose方法會調用Close來關閉流。可將using語句作爲一種清晰、異常安全以及可靠的方式來保證一個資源總是被釋放。 這解決了手動try/finally方案存在的所有問題。新方案具有以下特點。

(1)需要清理多個資源時, 具有良好的擴展性。

(2)不影響程序 代碼的邏輯。

(3)對問題進行良好抽象, 避免重複性編碼。

(4)非常健壯: using語句結束後,就不能使用using語句中聲明的變量(前一個例子是reader),因爲它已離開作用域。非要使用會產生編譯時錯誤。

 

參考書籍:《Visual C#從入門到精通》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章