.NET中棧和堆的比較 #1
原文出處:
http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory01122006130034PM/csharp_memory.aspx
儘管在.NET framework下我們並不需要擔心記憶體管理和記憶體回收(Garbage Collection),但是我們還是應該瞭解它們,以最佳化我們的應用程式。同時,還需要具備一些基礎的記憶體管理工作機制的知識,這樣能夠有助於解釋我們日常程式編寫中的變數的行為。在本文中我將講解棧和堆的基本知識,變數類型以及為什麼一些變數能夠按照它們自己的方式工作。
在.NET framework環境下,當我們的代碼執行時,記憶體中有兩個地方用來儲存這些代碼。假如你不曾瞭解,那就讓我來給你介紹棧(Stack)和堆(Heap)。棧和堆都用來協助我們運行代碼的,它們駐留在機器記憶體中,且包含所有代碼執行所需要的資訊。
* 棧vs堆:有什麼不同。
棧負責儲存我們的代碼執行(或調用)路徑,而堆則負責儲存對象(或者說資料,接下來將談到很多關於堆的問題)的路徑。
可以將棧想象成一堆從頂向下堆疊的盒子。當每調用一次方法時,我們將應用程式中所要發生的事情記錄在棧頂的一個盒子中,而我們每次只能夠使用棧頂的那個盒子。當我們棧頂的盒子被使用完之後,或者說方法執行完畢之後,我們將拋開這個盒子然後繼續使用棧頂上的新盒子。堆的工作原理比較相似,但大多數時候堆用作儲存資訊而非儲存執行路徑,因此堆能夠在任意時間被訪問。與棧相比堆沒有任何訪問限制,堆就像床上的舊衣服,我們並沒有花時間去整理,那是因為可以隨時找到一件我們需要的衣服,而棧就像儲物櫃裡堆疊的鞋盒,我們只能從最頂層的盒子開始取,直到發現那隻合適的。
[heapvsstack1.gif]
以上圖片並不是記憶體中真實的表現形式,但能夠協助我們區分棧和堆。
棧是自我維護的,也就是說記憶體自動維護棧,當棧頂的盒子不再被使用,它將被拋出。相反的,堆需要考慮記憶體回收,記憶體回收用於保持堆的整潔性,沒有人願意看到周圍都是贓衣服,那簡直太臭了。
* 棧和堆裡有些什麼。
當我們的代碼執行的時候,棧和堆中主要放置了四種類型的資料:實值型別(Value Type),參考型別(Reference Type),指標(Pointer),指令(Instruction)。
1.實值型別:
在C#中,所有被聲明為以下類型的事物被稱為實值型別:
bool
byte
char
decimal
double
enum
float
int
long
sbyte
short
struct
uint
ulong
ushort
2.參考型別:
所有的被聲明為以下類型的事物被稱為參考型別:
class
interface
delegate
object
string
3.指標:
在記憶體管理方案中放置的第三種類型是類型引用,引用通常就是一個指標。我們不會顯示的使用指標,它們由通用語言執行平台(CLR)來管理。指標(或引用)是不同於參考型別的,是因為當我們說某個事物是一個參考型別時就意味著我們是通過指標來訪問它的。指標是一塊記憶體空間,而它指向另一個記憶體空間。就像棧和堆一樣,指標也同樣要佔用記憶體空間,但它的值是一個記憶體位址或者為空白。
[heapvsstack2.gif]
4.指令:
在後面的文章中你會看到指令是如何工作的...
* 如何決定放哪兒?
這裡有一條黃金規則:
1. 參考型別總是放在堆中。(夠簡單的吧?)
2. 實值型別和指標總是放在它們被聲明的地方。(這條稍微複雜點,需要知道棧是如何工作的,然後才能斷定是在哪兒被聲明的。)
就像我們先前提到的,棧是負責儲存我們的代碼執行(或調用)時的路徑。當我們的代碼開始調用一個方法時,將放置一段編碼指令(在方法中)到棧上,緊接著放置方法的參數,然後代碼執行到方法中的被“壓棧”至棧頂的變數位置。通過以下例子很容易理解...
下面是一個方法(Method):
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
現在就來看看在棧頂發生了些什麼,記住我們所觀察的棧頂下實際已經壓入了許多別的內容。
首先方法(只包含需要執行的邏輯位元組,即執行該方法的指令,而非方法體內的資料)入棧,緊接著是方法的參數入棧。(我們將在後面討論更多的參數傳遞)
[heapvsstack3.gif]
接著,控制(即執行方法的線程)被傳遞到堆棧中AddFive()的指令上,
[heapvsstack4.gif]
當方法執行時,我們需要在棧上為“result”變數分配一些記憶體,
[heapvsstack5.gif]
The method finishes execution and our result is returned.
方法執行完成,然後方法的結果被返回。
[heapvsstack6.gif]
通過將棧指標指向AddFive()方法曾使用的可用的記憶體位址,所有在棧上的該方法所使用記憶體都被清空,且程式將自動回到棧上最初的方法調用的位置(在本例中不會看到)。
[heapvsstack7.gif]
在這個例子中,我們的"result"變數是被放置在棧上的,事實上,當實值型別資料在方法體中被聲明時,它們都是被放置在棧上的。
實值型別資料有時也被放置在堆上。記住這條規則--實值型別總是放在它們被聲明的地方。好的,如果一個實值型別資料在方法體外被聲明,且存在於一個參考型別中,那麼它將被堆中的參考型別所取代。
來看另一個例子:
假如我們有這樣一個MyInt類(它是參考型別因為它是一個類類型):
public class MyInt
{
public int MyValue;
}
然後執行下面的方法:
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
就像前面提到的,方法及方法的參數被放置到棧上,接下來,控制被傳遞到堆棧中AddFive()的指令上。
[heapvsstack8.gif]
接著會出現一些有趣的現象...
因為"MyInt"是一個參考型別,它將被放置在堆上,同時在棧上產生一個指向這個堆的指標引用。
[heapvsstack9.gif]
在AddFive()方法被執行之後,我們將清空...
[heapvsstack10.gif]
我們將剩下孤獨的MyInt對象在堆中(棧中將不會存在任何指向MyInt對象的指標!)
[heapvsstack11.gif]
這就是記憶體回收行程(後簡稱GC)起作用的地方。當我們的程式達到了一個特定的記憶體閥值,我們需要更多的堆空間的時候,GC開始起作用。GC將停止所有正在啟動並執行線程,找出在堆中存在的所有不再被主程式訪問的對象,並刪除它們。然後GC會重新組織堆中所有剩下的對象來節省空間的,並調整棧和堆中所有與這些對象相關的指標。你肯定會想到這個過程非常耗費效能,所以這時你就會知道為什麼我們需要如此重視棧和堆裡有些什麼,特別是在需要編寫高效能的代碼時。
Ok... 這太棒了, 當它是如何影響我的。
Good question.
當我們使用參考型別時,我們實際是在處理該類型的指標,而非該類型本身。當我們使用實值型別時,我們是在使用實值型別本身。聽起來很迷糊吧。
同樣,例子是最好的描述。
假如我們執行以下的方法:
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了呢。... 看看我們所做的然後就知道是怎麼回事了:
在第一例子中,一切都像計劃的那樣進行著:
public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}
[heapvsstack12.gif]
在第二個例子中,我們沒有得到"3"是因為變數"x"和"y"都同時指向了堆中相同的對象。
public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}
[heapvsstack13.gif]
希望以上內容能夠使你對C#中的實值型別和參考型別的基本區別有一個更好的認識,並且對指標及指標是何時被使用的有一定的基本瞭解。在系列的下一個部分,我們將深入記憶體管理並專門討論方法參數。
To be continued...