内容導讀
•概述
•當你聲明一個變量背後發生了什麼?
•堆和棧
•值類型和引用類型
•哪些是值類型,哪些是引用類型?
•裝箱和拆箱
•裝箱和拆箱的性能問題
一、概述
本文會闡述六個重要的概念:堆、棧、值類型、引用類型、裝箱和拆箱。本文首先會通過闡述當你定義一個變量之後系統内部發生的改變開始講解,然後将關注點轉移到存儲雙雄:堆和棧。之後,我們會探讨一下值類型和引用類型,并對有關于這兩種類型的重要基礎内容做一個講解。
本文會通過一個簡單的代碼來展示在裝箱和拆箱過程中所帶來的性能上的影響,請各位仔細閱讀。
二、當你聲明一個變量背後發生了什麼?
當你在一個.net應用程式中定義一個變量時,在ram中會為其配置設定一些記憶體塊。這塊記憶體有三樣東西:變量的名稱、變量的資料類型以及變量的值。
上面簡單闡述了記憶體中發生的事情,但是你的變量究竟會被配置設定到哪種類型的記憶體取決于資料類型。在.net中有兩種可配置設定的記憶體:棧和堆。在接下來的幾個部分中,我們會試着詳細地來了解這兩種類型的存儲。
三、存儲雙雄:堆和棧
為了了解棧和堆,讓我們通過以下的代碼來了解背後到底發生了什麼。
代碼隻有三行,現在我們可以一行一行地來了解到底内部是怎麼來執行的。
•line 1:當這一行被執行後,編譯器會在棧上配置設定一小塊記憶體。棧會在負責跟蹤你的應用程式中是否有運作記憶體需要
•line 2:現在将會執行第二步。正如棧的名字一樣,它會将此處的一小塊記憶體配置設定疊加在剛剛第一步的記憶體配置設定的頂部。你可以認為棧就是一個一個疊加起來的房間或盒子。在棧中,資料的配置設定和解除都會通過lifo (last in first out)即先進後出的邏輯規則進行。換句話說,也就是最先進入棧中的資料項有可能最後才會出棧。
•line 3:在第三行中,我們建立了一個對象。當這一行被執行後,.net會在棧中建立一個指針,而實際的對象将會存儲到一個叫做“堆”的記憶體區域中。“堆”不會監測運作記憶體,它隻是能夠被随時通路到的一堆對象而已。不同于棧,堆用于動态記憶體的配置設定。
•這裡需要注意的另一個重要的點是對象的引用指針是配置設定在棧上的。 例如:聲明語句 class1 cls1; 其實并沒有為class1的執行個體配置設定記憶體,它隻是在棧上為變量cls1建立了一個引用指針(并且将其預設職位null)。隻有當其遇到new關鍵字時,它才會在堆上為對象配置設定記憶體。
•離開這個method1方法時(the fun):現在執行控制語句開始離開方法體,這時所有在棧上為變量所配置設定的記憶體空間都會被清除。換句話說,在上面的示例中所有與int類型相關的變量将會按照“lifo”後進先出的方式從棧中一個一個地出棧。
•需要注意的是:這時它并不會釋放堆中的記憶體塊,堆中的記憶體塊将會由垃圾回收器稍候進行清理。
現在我們許多的開發者朋友一定很好奇為什麼會有兩種不同類型的存儲?我們為什麼不能将所有的記憶體塊配置設定隻到一種類型的存儲上?
如果你觀察足夠仔細,基中繼資料類型并不複雜,他們僅僅儲存像 ‘int i = 0’這樣的值。對象資料類型就複雜了,他們引用其他對象或其他基中繼資料類型。換句話說,他們儲存其他多個值的引用并且這些值必須一一地存儲在記憶體中。對象類型需要的是動态記憶體而基元類型需要靜态記憶體。如果需求是動态記憶體的話,那麼它将會在堆上為其配置設定記憶體,相反,則會在棧上為其配置設定。
四、值類型和引用類型
既然我們已經了解了棧和堆的概念了,是時候了解值類型和引用類型的概念了。值類型将資料和記憶體都儲存在同一位置,而一個引用類型則會有一個指向實際記憶體區域的指針。
通過下圖,我們可以看到一個名為i的整形資料類型,它的值被指派到另一個名為j的整形資料類型。他們的值都被存儲到了棧上。
當我們将一個int類型的值指派到另一個int類型的值時,它實際上是建立了一個完全不同的副本。換句話說,如果你改變了其中某一個的值,另一個不會發生改變。于是,這些種類的資料類型被稱為“值類型”。
當我們建立一個對象并且将此對象指派給另外一個對象時,他們彼此都指向了如下圖代碼段所示的記憶體中同一塊區域。是以,當我們将obj指派給obj1時,他們都指向了堆中的同一塊區域。換句話說,如果此時我們改變了其中任何一個,另一個都會受到影響,這也說明了他們為何被稱為“引用類型”。
五、哪些是值類型,哪些是引用類型?
在.net中,變量是存儲到棧還是堆中完全取決于其所屬的資料類型。比如:‘string’或‘object’屬于引用類型,而其他.net基中繼資料類型則會被配置設定到棧上。下圖則詳細地展示了在.net預置類型中,哪些是值類型,哪些又是引用類型。
六、裝箱和拆箱
現在,你已經有了不少的理論基礎了。現在,是時候了解上面的知識在實際程式設計中的使用了。在應用中最大的一個意義就在于:了解資料從棧移動到堆的過程中所發生的性能消耗問題,反之亦然。
考慮一下以下的代碼片段,當我們将一個值類型轉換為引用類型,資料将會從棧移動到堆中。相反,當我們将一個引用類型轉換為值類型時,資料也會從堆移動到棧中。
不管是在從棧移動到堆還是從堆中移動到棧上都會不可避免地對系統性能産生一些影響。
于是,兩個新名詞橫空出世:當資料從值類型轉換為引用類型的過程被稱為“裝箱”,而從引用類型轉換為值類型的過程則被成為“拆箱”。
如果你編譯一下上面這段代碼并且在ildasm(一個il的反編譯工具)中對其進行檢視,你會發現在il代碼中,裝箱和拆箱是什麼樣子的。下圖則展示了示例代碼被編譯後所産生的il代碼。
七、裝箱和拆箱的性能問題
為了弄明白到底裝箱和拆箱會帶來怎樣的性能影響,我們分别循環運作10000次下圖所示的兩個函數方法。其中第一個方法中有裝箱操作,另一個則沒有。我們使用一個stopwatch對象來監視時間的消耗。
具有裝箱操作的方法花費了3542毫秒來執行完成,而沒有裝箱操作的方法隻花費了2477毫秒,整整相差了1秒多。而且,這個值也會因為循環次數的增加而增加。也就是說,我們要盡量避免裝箱和拆箱操作。在一個項目中,如果你需要裝箱和裝箱,請仔細考慮它是否是絕對必不可少的操作,如果不是,那麼盡量不用。
雖然以上代碼段沒有展示拆箱操作,但其效果同樣适用于拆箱。你可以通過寫代碼來實作拆箱,并且通過stopwatch來測試其時間消耗。
原文出處: shivprasad koirala
譯文出處:edison chou(@周旭龍)
譯文連結:http://www.cnblogs.com/edisonchou/p/3947170.html