淺析C#程式設計中的記憶體管理

iDotNetSpace發表於2009-04-03

C#程式設計的一個優點是程式設計師不需要關心具體的記憶體管理,尤其是垃圾收集器會處理所有的記憶體清理工作。雖然不必手工管理記憶體,但如果要編寫高質量的程式碼,還是要理解後臺發生的事情,理解C#的記憶體管理。本文主要介紹給變數分配記憶體時計算機記憶體中發生的情況。

C#將資料分為兩種:值資料型別和引用資料型別,這兩種資料型別儲存在記憶體中的不同的地方:值資料型別儲存在堆疊中,而引用型別儲存在記憶體的託管堆中。

1、記憶體簡介
Windows使用一個系統:虛擬定址系統。這個系統的作用是將程式可用的記憶體地址對映到硬體記憶體中的實際地址上。其實際結果就是32位的機子上每個程式都可以使用4GB的記憶體,當然,64位機這個數字就大了去了。這4GB的記憶體實際上包含了程式的所有的部分:可執行程式碼,DLL以及程式執行時使用的所有變數的內容。這個4GB的記憶體成為虛擬地址空間或虛擬記憶體。為方便,這裡成為記憶體。

4GB中的每個儲存單元都是從零開始向上儲存的。要訪問儲存在記憶體中的某個空間中的值,就必須提供表示該儲存單元的一個數字。在高階程式語言中,編譯器的一個重要作用就是負責將人們可以理解的變數名稱變為處理器可以理解的記憶體地址。

2、堆疊
在記憶體中,有一個區域成為堆疊,儲存物件

物件成員的值資料型別呼叫方法時,傳遞給所有方法的引數的副本注意:呼叫方法時,堆疊儲存的是所有引數的副本,因此,經值型別A傳遞給函式,A的值是不會變化的。當然,引用型別是會變化的,因為在堆疊中儲存的是引用型別的地址,這在後面會有詳細的介紹。

下面以一個例子來說明堆疊的工作方式,如下面的程式碼:


{
   int a;
//do something;
{
int b;
//do something
}

}

首先宣告a,在內部的程式碼塊中宣告b,然後內部的程式碼塊終止,b就出了作用域,最後a出作用域。所以b的生命週期總是包含在a的生命週期內,在釋放變數的時候,其順序總是和分配記憶體的順序是相反的。即:變數的生存週期都是巢狀的。這就是堆疊的工作方式。

3、託管堆
堆疊具有相當高的效能,但是變數的生命週期必須是巢狀的,這個要求在有的時候過於苛刻。我們希望有一種別的方法來分配記憶體,儲存一些資料,並在方法退出的很長一段時間內,這些資料仍然是可用的,這時,就使用託管堆。

託管堆(簡稱堆)是記憶體中的另外一個區域,我們仍然用一個例子來說明堆的工作方式,如下面程式碼:


{
Customer customer1;
customer1=new Customer();
Customer customer2=new Customer();
//do something
}

首先,宣告一個Customer:customer1,在堆疊上給這個引用分配儲存控制元件。請注意:僅僅是給這個引用分配儲存空間,並不是實際的Customer物件。customer1佔用4個位元組的空間(32位機),來表示Customer物件在記憶體中的地址。

然後,執行第二行程式碼,完成以下操作:

在堆上分配儲存空間,用來儲存Customer物件,注意:這裡是Customer對像。

將變數customer1的值設為分配給Customer物件的記憶體地址從這個例子中可以看出,建立引用型別的變數的過程要比獎勵值型別變數的過程複雜,且不避免的有效能的降低。但是,我們可以將一個引用變數的值賦給另一個引用變數,當一個變數出作用域時,它會從堆疊中刪除,但是物件的資料仍然保留在記憶體中,直到程式停止。

這樣,我們在將一個引用變數A傳遞給函式時,僅僅是將變數A的引用傳遞給了函式,即:僅僅是在堆疊上分配記憶體,即變數B兩者指向同一個記憶體地址。因此,當變數B發生變化時,變數A也會發生變化。

4、裝箱和拆箱
裝箱和拆箱就是值型別和引用型別的專案轉化,裝箱可以將值型別轉化為引用型別,拆箱的作用正好相反,經引用型別轉化為值型別。

5、垃圾收集
一般情況下。NET執行庫會在認為需要的時候執行垃圾收集器來釋放託管資源,

這在大多數情況下,足夠了。就是說我們沒有必要去關心記憶體。但在有的情況下,我們會強制垃圾回收集器在程式碼的某個地方執行,釋放記憶體。這就用到了System.GC.Collect()。System.GC表示一個垃圾收集器。這種情況很少,例如:程式碼中大量的物件剛剛停止引用,就適合呼叫垃圾收集器。

總結

首先堆疊和堆(託管堆)都在程式的虛擬記憶體中。(在32位處理器上每個程式的虛擬記憶體為4GB)

堆疊stack
堆疊中儲存值型別。

堆疊實際上是向下填充,即由高記憶體地址指向地記憶體地址填充。

堆疊的工作方式是先分配記憶體的變數後釋放(先進後出原則)。

堆疊中的變數是從下向上釋放,這樣就保證了堆疊中先進後出的規則不與變數的生命週期起衝突!

堆疊的效能非常高,但是對於所有的變數來說還不太靈活,而且變數的生命週期必須巢狀。

通常我們希望使用一種方法分配記憶體來儲存資料,並且方法退出後很長一段時間內資料仍然可以使用。此時就要用到堆(託管堆)!

堆(託管堆)heap
堆(託管堆)儲存引用型別。

此堆非彼堆,.NET中的堆由垃圾收集器自動管理。

與堆疊不同,堆是從下往上分配,所以自由的空間都在已用空間的上面。

比如建立一個物件:

Customer cus;

cus = new Customer();

申明一個Customer的引用cus,在堆疊上給這個引用分配儲存空間。這僅僅只是一個引用,不是實際的Customer物件!

cus佔4個位元組的空間,包含了儲存Customer的引用地址。

接著分配堆上的記憶體以儲存Customer物件的例項,假定Customer物件的例項是32位元組,為了在堆上找到一個儲存Customer物件的儲存位置。

.NET執行庫在堆中搜尋第一個從未使用的,32位元組的連續塊儲存Customer物件的例項!

然後把分配給Customer物件例項的地址賦給cus變數!

從這個例子中可以看出,建立物件引用的過程比建立值變數的過程複雜,且不能避免效能的降低!

實際上就是.NET執行庫儲存對的狀態資訊,在堆中新增新資料時,堆疊中的引用變數也要更新。

效能上損失很多!

有種機制在分配變數記憶體的時候,不會受到堆疊的限制:把一個引用變數的值賦給一個相同型別的變數,那麼這兩個變數就引用同一個堆中的物件。

當一個應用變數出作用域時,它會從堆疊中刪除。但引用物件的資料仍然保留在堆中,一直到程式結束 或者 該資料不被任何變數應用時,垃圾收集器會刪除它。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-580347/,如需轉載,請註明出處,否則將追究法律責任。

相關文章