【轉】.NET中的六個重要概念:棧、堆、值型別、引用型別、裝箱和拆箱

李嶽恆-若冰羽軒發表於2014-10-09

內容導讀

概述 當你宣告一個變數背後發生了什麼? 堆和棧 值型別和引用型別 哪些是值型別,哪些是引用型別? 裝箱和拆箱 裝箱和拆箱的效能問題  一、概述

  本文會闡述六個重要的概念:堆、棧、值型別、引用型別、裝箱和拆箱。本文首先會通過闡述當你定義一個變數之後系統內部發生的改變開始講解,然後將關注點轉移到儲存雙雄:堆和棧。之後,我們會探討一下值型別和引用型別,並對有關於這兩種型別的重要基礎內容做一個講解。

  本文會通過一個簡單的程式碼來展示在裝箱和拆箱過程中所帶來的效能上的影響,請各位仔細閱讀。

1

 二、當你宣告一個變數背後發生了什麼?

  當你在一個.NET應用程式中定義一個變數時,在RAM中會為其分配一些記憶體塊。這塊記憶體有三樣東西:變數的名稱、變數的資料型別以及變數的值。

  上面簡單闡述了記憶體中發生的事情,但是你的變數究竟會被分配到哪種型別的記憶體取決於資料型別。在.NET中有兩種可分配的記憶體:棧和堆。在接下來的幾個部分中,我們會試著詳細地來理解這兩種型別的儲存。

2

 三、儲存雙雄:堆和棧

  為了理解棧和堆,讓我們通過以下的程式碼來了解背後到底發生了什麼。

1 2 3 4 5 6 7 8 9 10 11 public void Method1() { // Line 1 int i=4;

// Line 2
int y=2;

//Line 3
class1 cls1 = new class1();

}   程式碼只有三行,現在我們可以一行一行地來了解到底內部是怎麼來執行的。

Line 1:當這一行被執行後,編譯器會在棧上分配一小塊記憶體。棧會在負責跟蹤你的應用程式中是否有執行記憶體需要 Line 2:現在將會執行第二步。正如棧的名字一樣,它會將此處的一小塊記憶體分配疊加在剛剛第一步的記憶體分配的頂部。你可以認為棧就是一個一個疊加起來的房間或盒子。在棧中,資料的分配和解除都會通過LIFO (Last In First Out)即先進後出的邏輯規則進行。換句話說,也就是最先進入棧中的資料項有可能最後才會出棧。 Line 3:在第三行中,我們建立了一個物件。當這一行被執行後,.NET會在棧中建立一個指標,而實際的物件將會儲存到一個叫做“堆”的記憶體區域中。“堆”不會監測執行記憶體,它只是能夠被隨時訪問到的一堆物件而已。不同於棧,堆用於動態記憶體的分配。 這裡需要注意的另一個重要的點是物件的引用指標是分配在棧上的。 例如:宣告語句 Class1 cls1; 其實並沒有為Class1的例項分配記憶體,它只是在棧上為變數cls1建立了一個引用指標(並且將其預設職位null)。只有當其遇到new關鍵字時,它才會在堆上為物件分配記憶體。 離開這個Method1方法時(the fun):現在執行控制語句開始離開方法體,這時所有在棧上為變數所分配的記憶體空間都會被清除。換句話說,在上面的示例中所有與int型別相關的變數將會按照“LIFO”後進先出的方式從棧中一個一個地出棧。 需要注意的是:這時它並不會釋放堆中的記憶體塊,堆中的記憶體塊將會由垃圾回收器稍候進行清理。 3

  現在我們許多的開發者朋友一定很好奇為什麼會有兩種不同型別的儲存?我們為什麼不能將所有的記憶體塊分配只到一種型別的儲存上?

  如果你觀察足夠仔細,基後設資料型別並不複雜,他們僅僅儲存像 ‘int i = 0’這樣的值。物件資料型別就複雜了,他們引用其他物件或其他基後設資料型別。換句話說,他們儲存其他多個值的引用並且這些值必須一一地儲存在記憶體中。物件型別需要的是動態記憶體而基元型別需要靜態記憶體。如果需求是動態記憶體的話,那麼它將會在堆上為其分配記憶體,相反,則會在棧上為其分配。

4

 四、值型別和引用型別

  既然我們已經瞭解了棧和堆的概念了,是時候瞭解值型別和引用型別的概念了。值型別將資料和記憶體都儲存在同一位置,而一個引用型別則會有一個指向實際記憶體區域的指標。

  通過下圖,我們可以看到一個名為i的整形資料型別,它的值被賦值到另一個名為j的整形資料型別。他們的值都被儲存到了棧上。

  當我們將一個int型別的值賦值到另一個int型別的值時,它實際上是建立了一個完全不同的副本。換句話說,如果你改變了其中某一個的值,另一個不會發生改變。於是,這些種類的資料型別被稱為“值型別”。

5

  當我們建立一個物件並且將此物件賦值給另外一個物件時,他們彼此都指向瞭如下圖程式碼段所示的記憶體中同一塊區域。因此,當我們將obj賦值給obj1時,他們都指向了堆中的同一塊區域。換句話說,如果此時我們改變了其中任何一個,另一個都會受到影響,這也說明了他們為何被稱為“引用型別”。

 五、哪些是值型別,哪些是引用型別?

  在.NET中,變數是儲存到棧還是堆中完全取決於其所屬的資料型別。比如:‘String’或‘Object’屬於引用型別,而其他.NET基後設資料型別則會被分配到棧上。下圖則詳細地展示了在.NET預置型別中,哪些是值型別,哪些又是引用型別。

6

 六、裝箱和拆箱

  現在,你已經有了不少的理論基礎了。現在,是時候瞭解上面的知識在實際程式設計中的使用了。在應用中最大的一個意義就在於:理解資料從棧移動到堆的過程中所發生的效能消耗問題,反之亦然。

  考慮一下以下的程式碼片段,當我們將一個值型別轉換為引用型別,資料將會從棧移動到堆中。相反,當我們將一個引用型別轉換為值型別時,資料也會從堆移動到棧中。

  不管是在從棧移動到堆還是從堆中移動到棧上都會不可避免地對系統效能產生一些影響。

  於是,兩個新名詞橫空出世:當資料從值型別轉換為引用型別的過程被稱為“裝箱”,而從引用型別轉換為值型別的過程則被成為“拆箱”。

7

  如果你編譯一下上面這段程式碼並且在ILDASM(一個IL的反編譯工具)中對其進行檢視,你會發現在IL程式碼中,裝箱和拆箱是什麼樣子的。下圖則展示了示例程式碼被編譯後所產生的IL程式碼。

8

 七、裝箱和拆箱的效能問題

  為了弄明白到底裝箱和拆箱會帶來怎樣的效能影響,我們分別迴圈執行10000次下圖所示的兩個函式方法。其中第一個方法中有裝箱操作,另一個則沒有。我們使用一個Stopwatch物件來監視時間的消耗。

  具有裝箱操作的方法花費了3542毫秒來執行完成,而沒有裝箱操作的方法只花費了2477毫秒,整整相差了1秒多。而且,這個值也會因為迴圈次數的增加而增加。也就是說,我們要儘量避免裝箱和拆箱操作。在一個專案中,如果你需要裝箱和裝箱,請仔細考慮它是否是絕對必不可少的操作,如果不是,那麼儘量不用。

10

  雖然以上程式碼段沒有展示拆箱操作,但其效果同樣適用於拆箱。你可以通過寫程式碼來實現拆箱,並且通過Stopwatch來測試其時間消耗。

相關文章