.Net中堆和棧的理解
什麼是棧堆
執行緒堆疊:簡稱棧 Stack:一種資料項按序排列的資料結構,只能在一端(稱為棧頂(top))對資料項進行插入和刪除。像是往一個盒子裡面放東西,先放進去的在最低層,後面放上去的在最上面,想拿到最下面的就要先拿掉它上面的。其實,它就是一種後進先出的資料結構。
託管堆: 簡稱堆 Heap:是電腦科學中一類特殊的資料結構的統稱。堆通常是一個可以被看做一棵完全二叉樹的陣列物件。
像是一顆倒立的大樹,堆是一種經過排序的樹形資料結構,每個節點都有一個值。通常我們所說的堆的資料結構是指二叉樹。堆的特點是根節點的值最小(或最大),且根節點的兩個樹也是一個堆。由於堆的這個特性,常用來實現優先佇列,堆的存取是隨意的,這就如同我們在圖書館的書架上取書,雖然書的擺放是有順序的,但是我們想取任意一本時不必像棧一樣,先取出前面所有的書,書架這種機制不同於箱子,我們可以直接取出我們想要的書。
棧和堆的區別
堆疊空間分配
棧(作業系統):由作業系統自動分配釋放,存放函式的變數值,區域性變數的值等等,其操作方式類似於資料結構中的棧;
堆(作業系統):一般由開發者分配釋放,若不釋放,程式結束時可能會有OS回收,分配方式倒是類似於連結串列;
堆疊的快取方式
棧使用的是一級快取,通常是被呼叫時處於儲存空間,呼叫完後自動釋放;
堆使用的是二級快取,生命週期有虛擬機器的垃圾回收演算法來決定(並不一定成為孤兒物件就被立即釋放)。所以呼叫這些物件的速度相對來得慢一些;
堆疊資料結構的區別
堆(資料結構):堆可以看做是一棵樹;例如:堆排序;
棧(資料結構):一種先進後出的資料結構;
區別介紹
棧負責儲存我們的程式碼執行(或呼叫)路,而堆則負責儲存物件(或者說資料)的路徑。
可以將棧想象成一堆從頂向下堆疊的盒子。當每呼叫一次方法時,我們將應用程式中所要發生的事情記錄在棧頂的一個盒子中,而我們每次只能夠使用棧頂的那個盒子。當我們棧頂的盒子被使用完之後,或者說方法執行完畢之後,我們將拋開這個盒子然後繼續使用棧頂上的新盒子。堆的工作原理比較相似,但大多數時候堆用作儲存資訊而非儲存執行路徑,因此堆能夠在任意時間被訪問。與棧相比堆沒有任何訪問限制,堆就像床上的舊衣服,我們並沒有花時間去整理,那是因為可以隨時找到一件我們需要的衣服,而棧就像儲物櫃裡堆疊的鞋盒,我們只能從最頂層的盒子開始取,直到發現那隻合適的。
以上圖片並不是記憶體中真實的表現形式,但能夠幫助我們區分棧和堆。
棧是自行維護的,也就是說記憶體自動維護棧,當棧頂的盒子不再被使用,它將被丟擲。相反的,堆需要考慮垃圾回收,垃圾回收用於保持堆的整潔性,沒有人願意看到周圍都是贓衣服,那簡直太臭了!
當我們的程式碼執行的時候,棧和堆中主要放置了四種類型的資料:值型別(Value Type),引用型別(Reference Type),指標(Pointer),指令(Instruction)。
1.值型別
bool,byte ,char,decimal,double,enum,float,int,long,sbyte,short,struct,uint,ulong,ushort
2.引用型別
class,interface,delegate ,object ,string
3.指標
在記憶體管理方案中放置的第三種類型是型別引用,引用通常就是一個指標。我們不會顯示的使用指標,它們由公共語言執行時(CLR)來管理。指標(或引用)是不同於引用型別的,是因為當我們說某個事物是一個引用型別時就意味著我們是通過指標來訪問它的。指標是一塊記憶體空間,而它指向另一個記憶體空間。就像棧和堆一樣,指標也同樣要佔用記憶體空間,但它的值是一個記憶體地址或者為空。
如上圖,可以幫我們更加好理解堆和棧,在裝箱和拆箱中也能體現
堆疊使用情況
1.引用型別總是放在堆中。
2.值型別和指標總是放在它們被宣告的地方。
就像我們先前提到的,棧是負責儲存我們的程式碼執行(或呼叫)時的路徑。當我們的程式碼開始呼叫一個方法時,將放置一段編碼指令(在方法中)到棧上,緊接著放置方法的引數,然後程式碼執行到方法中的被“壓棧”至棧頂的變數位置。通過以下例子很容易理解...
定義一個方法
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
現在就來看看在棧頂發生了些什麼,記住我們所觀察的棧頂下實際已經壓入了許多別的內容。
首先方法(只包含需要執行的邏輯位元組,即執行該方法的指令,而非方法體內的資料)入棧,緊接著是方法的引數入棧。
接著,控制(即執行方法的執行緒)被傳遞到堆疊中AddFive()的指令上
當方法執行時,我們需要在棧上為“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;
}
就像前面提到的,方法及方法的引數被放置到棧上,接下來,控制被傳遞到堆疊中AddFive()的指令上。
接著會出現一些有趣的現象...
因為"MyInt"是一個引用型別,它將被放置在堆上,同時在棧上生成一個指向這個堆的指標引用。
在AddFive()方法被執行之後,我們將清空...
我們將剩下孤獨的MyInt物件在堆中(棧中將不會存在任何指向MyInt物件的指標!)
這就是垃圾回收器(後簡稱GC)起作用的地方。當我們的程式達到了一個特定的記憶體閥值,我們需要更多的堆空間的時候,GC開始起作用。GC將停止所有正在執行的執行緒,找出在堆中存在的所有不再被主程式訪問的物件,並刪除它們。然後GC會重新組織堆中所有剩下的物件來節省空間,並調整棧和堆中所有與這些物件相關的指標。你肯定會想到這個過程非常耗費效能,所以這時你就會知道為什麼我們需要如此重視棧和堆裡有些什麼,特別是在需要編寫高效能的程式碼時。
當我們使用引用型別時,我們實際是在處理該型別的指標,而非該型別本身。當我們使用值型別時,我們是在使用值型別本身。
案例:
執行以下方法
?1 2 3 4 5 6 7 8 9 |
public int ReturnValue()
{
int x = new int ();
x = 3;
int y = new int ();
y = x;
y = 4;
return x;
}
|
最後結果為3
假如我們首先使用MyInt類
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了呢?... 看看我們所做的然後就知道是怎麼回事了:
在第一例子中,一切都像計劃的那樣進行著:
在第二個例子中,我們沒有得到"3"是因為變數"x"和"y"都同時指向了堆中相同的物件。