標籤:
原文:譯文---C#堆VS棧(Part One)
前言
本文主要是講解C#語言在記憶體中堆、棧的使用方式,使讀者能更好的理解實值型別、參考型別以及線程棧、託管堆。
首先感謝原文Matthew Cochran 為我們帶來了一篇非常好的文章,並配以大量圖示,協助我們更好的理解堆棧之間的調用,本文是在作者原文的基礎上進行內容上的精簡以及加入我個人在這方面的理解和注釋。
最後要感謝部落格園的田志良,當我搜尋堆棧內部使用時,搜尋到了作者的文章,吸取了大量有用的知識,而且翻譯的也非常好。唯一美中不足的可能是僅僅翻譯了Matthew Cochran這個系列文章的第一篇,而忽略了後面的幾篇,導致內容上略有不完整,所以,我會在繼續完成後續的工作,為大家答疑解惑。
下面引用作者寫作此文的原因:
雖然有了.Net Framework我們不用擔心記憶體管理和記憶體回收問題,但是我們仍然要記住記憶體管理和記憶體回收為了最佳化程式。而且,有一個記憶體管理如何啟動並執行基本概念將協助我們解釋我們寫的每一個程式中變數的行為。
註:限於本人英文理解能力,以及技術經驗,文中如有錯誤之處,還請各位不吝指出。
目錄
C#堆棧對比(Part One)
C#堆棧對比(Part Two)
C#堆棧對比(Part Three)
C#堆棧對比(Part Four)
棧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回收的規則,很多堆上的內容在程式退出前都沒有被回收,很可能是無意中某處還保留著內容的引用導致,這將嚴重影響效能。
- 實值型別與參考型別在改變內容時處理的方式不同:實值型別執行內容拷貝,參考型別始終更改的是所引用的內容,這將導致兩者行為上的不一致。
譯文---C#堆VS棧(Part One)