程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> .NET的內存管理詳解教程

.NET的內存管理詳解教程

編輯:關於C#
 

盡管運行庫負責為程序員處理大部分內存管理工作,但程序員仍必須理解內存管理的工作原理,了解如何處理未托管的資源。


       一、運行庫如何在堆棧和堆上分配空間

      c#編程的一個優點就是程序員無需擔心具體的內存管理,因為垃圾收集器會處理所有的內存清理工作。但是如果要編寫高效的c#代碼,就必須理解後台發生的事情。
      Windows使用一個系統:虛擬尋址系統,該系統把程序可用的內存地址隱射到硬件內存中的實際地址上,這些任務完全由Windows在後台管理,其實際結果是32位的處理器的每個進程都可以使用4GB的內存。要訪問存儲在內存中某個空間中的某個值,就需要提供表示該存儲單元的數字,在高級語言中如c#,編譯器負責把人們可以理解的變量名轉換為處理器可以理解的內存地址。
      在進程的虛擬內存中,有一個區域稱為堆棧。堆棧存儲不是對象成員的值數據類型。在調用一個方法時,也使用堆棧來存儲傳遞給方法的所有參數復本。需要注意c#中變量的作用域,如果變量a在變量b之前進入作用域,b就會先退出作用域,如下面的代碼:

{

    int a;

    {
          int b;

    }

}

     代碼中先聲明a,然後在代碼內部聲明b,當內部的代碼塊終止,b退出作用域,最後a退出作用域,所以b的生命周期完全包含在a的生存期中。在釋放變量時,其順序總是與給他們分配內存的順序相反,這就是堆棧的工作原理。
     我們不知道堆棧在地址空間的什麼地方,這些信息在進行C#開發是不需要知道的。堆棧指針(操作系統維護的一個變量)表示堆棧中下一個自由空間地址。程序第一次運行時,堆棧指針指向為堆棧保留的內存塊的末尾。堆棧實際上是向下填充的,即從高內存地址向低內存地址填充,當數據入棧後,堆棧指針會自動隨之調整,保證始終指向下一個自由空間,如下圖所示,該圖中堆棧指針800000的下一個自由空間是地址799999.



 

   我們看以下代碼:

           {

             int a = 10;
             double b = 3000.0;

}

 上面的代碼在運行時會告訴編譯器,需要一些內存的存儲單元存儲一個整數和一個雙精度浮點數,這些存儲單元會分別分配給a和b,聲明每個變量的代碼表示請求訪問這個變量,閉合花括號表示這兩個變化量出作用域的地方     
     如果使用上圖所示的堆棧才存儲a和b,因為a是整形變量需要4個字節存儲,所以a的賦值10放在799996~799999上。堆棧指針對自動下移4個字節的單元,指向位置799996,即下一個自由空間為799995.double類型的變量b要占用8個字節,所以堆棧指針再下移8個字節單元。
     堆棧有非常高的性能,但對於所有的變量並不夠靈活。在堆棧存儲的變量中,變量的生存期必須嵌套,在許多情況下,這種要求過於苛刻。通常我們希望使用一個方法分配內存,來存儲一些數據,並在方法退出後的很長一段時間內數據有效。這種情況可以使用new運算符來請求存儲空間,此時就要用到托管堆。
     托管堆和C++下的堆不用,因為它是在垃圾收集器的控制下工作的,其與傳統的堆相比具有顯著性能優勢。
     托管堆(簡稱堆)是進程的可用4GB中的另一個內存區域。下面代碼說明堆的工作原理和如何為引用數據類型分配內存。

void DoWork()

{
       Customer arable;
       arable = new Customer();
     }

上面代碼中,假定存在類Customer。

 首先,聲明了一個Customer類型的引用arabel,在堆棧上給這個引用分配存儲空間,注意這只是引用而不是實際的Customer對象。arabel引用占用了4個字節的空間,包含了存儲Customer對象的地址(需要4個字節把內存地址表示為0到4GB之間的一個整數值)。
arabel = new Customer();
    這行代碼首先在堆上分配內存,以存儲Customer的實例(一個真正的實例,不只是一個地址),然後把變量arabel的值設置為分配給新Customer對象的內存地址。為了在堆上找到一個存儲新Customer對象的存儲位置,.NET運行庫在堆中搜索,選取第一個未使用的、32字節(這裡假定Customer對象占用32字節存儲)的連續塊。為了討論方便,假定其地址是200000,arabel引用占用堆棧中的799996-7999999位置,在實例化arabel對象前,內存的內容如下圖所示:

 


 

     給Customer對象分配內存後,內存內容應如下圖所示,注意,與堆棧不同,堆上的內存是向上分配的,所以自由空間在已用空間的上面。

 

 

   二、垃圾收集的工作原理

     在垃圾收集器運行時,會在堆中刪除不再引用的所有對象。在完成刪除後,堆會立即把對象分散開來,與已經釋放的內存混合在一起,如下圖所示:

    

      如果托管的堆也是這樣,在其上給新的對象分配內存就會難以處理,因為運行庫必須搜索整個堆,才能找到足夠大的內存塊來存儲每個新對象。但是,垃圾收集器不會讓堆處於這種狀態。只要它釋放了能釋放的所有對象,就會壓縮其它對象,把它們都移動回堆的端部,再次形成一個連續的塊。在移動對象時,這些對象的所有引用都需要用正確的新地址來更新,但垃圾收集器也會處理更新的問題。
     垃圾收集器的這個壓縮操作是托管的堆與舊未托管的堆的區別所在。使用托管的堆,就只需要讀取堆指針的值即可,而不是搜索鏈接地址列表,來查找要一個地方來放置新數據。因此在.NET下實例化對象要快得多。有趣的是,訪問它們的速度也比較快,因為對象會壓縮到堆上相同的內存區域,這樣需要交換的頁面較少。微軟相信,盡管垃圾收集器需要做一些工作,如壓縮堆,修改它移動的所有對象的引用,導致性能降低,但這些性能會得到彌補。
      垃圾收集器不知道如何釋放未托管的資源(例如文件句柄、網絡連接和數據庫連接)。托管類在封裝對未托管資源的直接或間接引用時,需要定制專門的規則,確保未托管的資源在回收類的一個實例時釋放。

      三、如何使用析構函數和System.IDisposable接口來確保正確釋放未托管的資源

在定義一個類時,可以使用兩種機制來自動釋放未托管的資源。

(1)    聲明一個析構函數

(2)    在類中執行System.IDisposable接口

在調用垃圾收集器刪除對象之前,可以調用析構函數。

析構函數看起來類似於一個方法,與包含類同名,但前面加上(~)。它沒有返回值,沒有參數,沒有訪問修飾符,下面是一個例子:

Class MyClass
{

    ~MyClass()

{

}

}

C# 編譯器在編譯析構函數時,會隱式地把析構函數的代碼編譯為Finalize()方法對應的代碼,確保執行父類的Finalize()方法。下面列出了編譯器為~MyClass()析構函數生成的IL的對應的c#代碼:

protected override void Finalize()

{

     try
         {}
    finally

{base.Finalize();}

}

     C#析構函數的問題是它們的不確定性,由於垃圾收集器的工作方式,無法確定析構函數何時得到執行,所以不能在析構函數中放置需要在某一時刻運行的代碼,也不應使用能以任意順序對不同類實例調用的析構函數。另外一個問題是c#析構函數的執行會延遲對象最終從內存刪除的時間。有析構函數的對象需要垃圾收集器調用兩次才真正刪除對象。另外,運行庫使用一個線程來執行所有對象的Finalize()方法。如果頻繁使用析構函數,而且它們執行時間的清理任務,對性能的影響就會非常顯著。
      在c#中推薦使用System.IDisposable接口來替代析構函數。IDisposable聲明了一個方法Dispose(),它不帶參數,返回void,MyClass的方法Dispose()的執行代碼如下:

class MyClass : IDisposabale{

    public void Dispose()

        {}

}

Dispose()的執行代碼顯示釋放由對象直接使用的所有非托管資源,為對象釋放未托管資源提供精確的控制。

我們使用一個對象後可以調用這個對象的Dispose()方法來釋放未托管的資源。但是如果在處理過程中出現異常,就無法釋放這個對象使用的資源,所以應該使用try塊來編寫,在finally的代碼塊對資源進行釋放。C#提供了一種語法,可以確保在執行IDisposable接口的對象的引用超出作用域時,在該對象上自動調用Dispose(),該語法使用了using關鍵字來完成這一工作。這種情況下生成的IL代碼和使用try塊生成的是一樣的。

using(MyClass mc = new MyClass())

{ }

using語句的後面是一對圓括號,其中是引用變量的聲明和實例化,該語句使變量放在隨附的語句塊中,在變量超出作用域時,即使出現異常,也會自動調用Dispose()方法。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved