棧vs堆,最詳細的對比
棧vs堆:不同之處
棧負責追蹤那些在我們代碼中執行的內容(或者是那些被調用的內容)。而堆則負責追蹤我們的對象(我們的數據,當然大多數情況下都是“數據”,稍後我會討論這個問題)。
註:棧類似於代碼執行過程的一個容器,而堆則類似於保存數據的容器。
把棧想象成一個一系列的盒子,一個落著一個在上面。當我們每次調用一個方法(called a Frame)時,我們通過將盒子疊加在最頂部的盒子上來觀察在我們的代碼中究竟發生了什麽。其實,我們只能使用棧中最頂部的盒子。當我們處理完最頂部的盒子(我們執行過的方法、函數)後我們就丟棄它並且繼續使用之前在頂部的盒子。堆對於棧很相似,只是堆的目的是保存信息(大多數情況下不追蹤執行代碼),所以在任何時刻我們的堆都能被訪問。有了堆,我們將不像棧一樣有那麽多訪問約束。堆更像是一堆在床上我們還沒來得及整理的洗幹凈的衣服;我們能快速的得到我們想要的衣服。而棧更像是在壁櫥裏的一摞鞋盒,我們拿掉最頂上的鞋盒為了得到下面的盒子裏的鞋。
註:網上找了兩個圖片替代作者的圖片,這樣會更生動些。左側為棧,右側為堆。
棧可自我維護,這意味著它基本只關系它自己的內存管理。當棧頂的盒子不再使用之後,隨即就丟棄掉。堆,在另一方面而言,必須關心垃圾回收問題,這些問題主要是處理如何保持堆整潔(沒有人喜歡亂堆臟衣服,臭氣熏天~~~)。
註:棧中的內容是每執行一次指令之後即釋放掉,所以無需關註資源泄漏;堆則需要GC不定時的回收已不再使用的資源,需維護並關註性能問題。
堆棧上究竟發生了什麽
在我們的堆或者棧中我們有四個主要類型:值類型、引用類型、指針類型和指令。
- 值類型:
在C#中,所有的值類型均繼承自System.ValueType這個抽象類。
註:System.ValueType繼承自System.Object,並且重寫了.ToString()等方法,以便阻止某些情況下的裝箱問題。
- bool
- byte
- char
- decimal
- double
- enum
- float
- int
- long
- sbyte
- short
- struct
- uint
- ulong
- ushort
2. 引用類型:
在C#中,如下的引用類型均繼承自System.Object,當然除了Object其自身。
- class
- interface
- delegate
- object
- string
3. 指針類型:
放置在我們內存管理中的第三個類型是一個引用類型,這就是我們常說的指針。我們不明確的使用指針,他們是被CLR管理的資源。指針(或者引用)是不同於引用類型的,當我們在討論引用類型的時候,就是說我們是通過指針使用引用類型的。一個指針是指向另一個內存空間的一大塊內存。一個指針占據空間就像是一個我們放置在堆或者棧中,並且它的值是一個地址或者為Null。
4. 指令類型:
稍後,在後面的文章中我們會分析。
如何推斷類型是在堆上,還是在棧上?
這裏,我們有兩條黃金定律:
- 引用類型總是在堆上創建,十分簡單,是吧?
- 值類型和指針類型總是在它聲明的地方創建。這有點復雜並且需要懂一點棧是如何工作的。
棧,正如我們前面講的,是負責追蹤每一個線程中代碼執行情況(或者被調用)。你可以認為它是一個線程狀態並且每一個線程有它自己的狀態。當我們的代碼調用執行一個方法,開始執行一個已經被JIT編譯過的指令,並且存活在方法表中(live on the method table),它也將參數放置在線程棧中。然後,當我們進入方法體並且帶著參數執行方法時,指令將被提到棧頂部。
下面我們用一段代碼來演示:
1 public int AddFive(int pValue) 2 { 3 int result; 4 result = pValue + 5; 5 return result; 6 }
這就是發生在棧上的事情。必須記住的是我們正在觀察的是已經存在於棧上的:我們執行方法並且方法參數被放置在棧中,稍後我們談論參數細節。
此圖中的AddFive方法並不存在於棧中,這裏只是為了演示說明。
下一步,命令執行到存在於我們的類型表中的AddFive()方法,如果第一次執行該方法,JIT會執行一次。
當方法執行時,我們需要一些內存為“result”這個變量,並且這個變量將在棧上創建,如下圖:
方法執行完畢,返回結果。如下圖:
所有在棧上創建的內存將被清理,通過將指針指向一開始AddFive()指向的可用內存地址。
在這個例子中,我們的“result”變量將被放置在棧中。事實上,每次當值類型帶著方法體被聲明時,它將被放置在棧中。
現在,值類型有時也被放置在堆中。請記住這個規則,值類型是根據其聲明的地方而決定其是在堆還是在棧上的。如果一個值類型在方法體外面聲明的,但是在引用類型內部,這樣它將被包裹在引用類型,並且在堆上創建。
例子如下:
如果我們有如下的MyInt類(類自然是一個引用類型)
public class MyInt { public int MyValue; } 執行如下: public MyInt AddFive(int pValue) { MyInt result = new MyInt(); result.MyValue = pValue + 5; return result; }
就像剛才一樣,線程開始執在線程棧上的行方法和參數,如下圖:
現在就比較有趣了。因為MyInt是一個引用類型,MyInt類型將被放置在堆中,並且被一個放置在棧中的指針所引用(指向),如下圖:
在AddFive()被執行後,我們將清理棧,如下圖:
我們只剩下一個孤獨的對象在堆中(在棧中將沒有任何指針指向堆中的MyInt),如下圖:
這就是GC展現實力的舞臺。當我們達到一定內存瓶頸時我們需要堆中要有更多的空間,這時GC出場。GC將停止所有運行中的線程(完全停止),找出在堆中所有沒有被引用的對象並且刪除它們。GC將重新組織所有在堆中的對象以獲得空間,調整所有在堆以及棧中的指針。就像你想象的那樣,這將花費十分昂貴的性能,所以現在你就能看出當你在寫高性能代碼時,關註堆棧中有什麽是如此的重要。
註:1.GC回收一般發生在程序內存不夠用時,否則不會發生除非手動調用。2.手動調用GC可實現強制“嘗試”回收資源。3.GC中的所有資源是分“代”的,每次檢測堆中的對象是否還有引用,如果有當前的“代”數加一,否則減一,GC回收“代”數最小的資源,這也就解釋了為什麽即使我手動調用GC.Collect()方法之後,對象還是沒有馬上被回收的問題。4.頻繁調用GC.Collect()會導致頻繁的線程中斷,從而嚴重影響性能。
好的,十分棒,跟我有什麽關系?
問的好。
當我們用引用類型時,我們正在處理指針這個類型,而不是引用(實際的方法、類型)其本身;當我們用值類型時,我們就是用的類型自身。這令人費解,對嗎?
再來,下面這個例子很好的詮釋了這個問題:
public int ReturnValue()
{ int x = new int(); x = 3; int y = new int(); y = x; y = 4; return x; }
最終的結果是3. 十分簡單,不是嗎?
public class MyInt { public int MyValue; } public int ReturnValue2() { MyInt x = new MyInt(); x.MyValue = 3; MyInt y = new MyInt(); y = x; y.MyValue = 4; return x.MyValue; }
返回值是什麽?答案是4!
為什麽?…x.MyValue是如何變為4的?看一看我們正在做的是什麽並且是否這樣做有意義:
public int ReturnValue() { int x = 3; int y = x; y = 4; return x; }
註:值類型是傳遞值,而非傳遞引用,如下圖:
下一個例子,我們沒得到“3”,因為x和y都是指向同一個堆對象的變量。
public int ReturnValue2() { MyInt x; x.MyValue = 3; MyInt y; y = x; y.MyValue = 4; return x.MyValue; }
註:實際來講,引用類型則是指向堆中的同一個對象。
希望這能讓您對值類型和引用類型有一個更好的理解通過C#代碼並且理解指針的用法和在那裏使用。
在下一部分(Part Two),我們將更深入的聊一聊內存管理,尤其要是討論方法參數。
總結
- 堆與棧的概念及不同點:在內存中棧主要負責處理線程中的命令,並且是以棧Stack的形式讀取與執行的;堆主要是存儲方法體以及數據,類似於床上散落的衣服,可供隨機讀取。
- 值類型與引用類型不同點:引用類型永遠存在於托管堆上,值類型在哪取決於聲明的位置。
- 堆和棧上的垃圾回收:棧有自我維護特性,執行完語句馬上釋放不會造成資源泄漏。堆則需GC回收,並且符合GC回收的規則,很多堆上的內容在程序退出前都沒有被回收,很可能是無意中某處還保留著內容的引用導致,這將嚴重影響性能。
- 值類型與引用類型在改變內容時處理的方式不同:值類型執行內容拷貝,引用類型始終更改的是所引用的內容,這將導致兩者行為上的不一致。
棧vs堆,最詳細的對比