Stack vs Heap: what’s the difference?
Stack或多或少的負責記錄正在我們代碼中執行的指令和數據, heap或多或少的負責記錄我們的Object的一些數據。
可以把Stack想象爲一個在另外一個的上面堆起來的盒子,當我們調用一個方法的時候,我們把這個方法(called a frame)象盒子一樣堆積在已經調用的方法之上,通過這種方式我們跟蹤了我們Application中代碼的執行情況。我們只能使用在Stack上面的盒子,當我們執行完畢正在運行的代碼的時候,我們把它從Stack中扔到一邊,然後進入Stack上的最上面的代碼繼續執行。 heap的情況類似,除了他的主要目的是爲了保存數據, 沒有像Stack一樣的訪問限制,可以按照任意順序訪問。 形象示意如下:
Stack是自我維護的,意味着基本上不需要我們手動進行內存管理,它有自己的內存管理體系(不管是OS來分配還是釋放), 而heap則是我們需要關係,和垃圾收集存在一定關係的。
What goes on the stack and heap?
當代碼執行的時候我們有四種類型的東西是要放在Stack和Heap上的,他們是: value types, reference types, pointers, and Instructions.
- Value Types
在C#中,下列的內容都屬於Value Type,這些類型繼承自System.ValueType, 除此之外都不算Value Types.
Bool, byte , char, decimal, double, enu, float, int, long, sbyte, short, struct, uint, ulong, ushort - Reference Types
使用下列類型聲明的對象都是Reference Type,包括所有的從System.Object繼承下來的類型(System.ValueType除外)或者屬於下列類型的。
class, interface, delegate, object,string. - Pointers
在內存管理中涉及的第三個數據類型是引用。引用經常被當做指針,我們並不會明確的使用指針,指針是被CLR管理的。 一個指針或者引用是和引用類型不相同的,一個指針是內存中一段內存塊,保存着指向內存中的另一個地址。引用和我們放在Stack以及Heap中的其他東西一樣佔用空間,他的值爲一段內存地址或者NULL。如下圖所示。
- Instructions
一會再說。。。
How is it decided what goes where ?
有兩條規則:
- 引用類型總是在Heap上。
- Value Typ和Pointer是在他們被聲明的地方上。
stack負責記錄追蹤我們執行代碼中的線程情況,當我們調用一個方法的時候,執行線程開始執行JIT編譯過的方法表上的指令,同時將方法的參數放到線程的Stack上,然後當我們採用這些變量進入方法的時候,他們就被放到了Stack的頂部。
假如執行如下代碼:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
接下來是在Stack的頂部會發生什麼。記住我們要查看的是早已經放到Stack頂部的其他的東西。 一旦我們開始執行方法,方法的參數就會被放到Stack上。
方法執行完畢後,Result被返回。
然後在Stack上分配的內存被清理掉,Stack的指針被移到AddFive方法開始的地方,然後回到了Stack之前的方法上。
在這個示例中,“Result”變量是放在Stack上的。事實上,任何一個在方法體中聲明的Value Type都會被放到Stack上。
現在,Value Types 有時候會被放到Heap上。 記住這條規則: Value Type always go where they were declared, Well, 假如一個值類型是在方法的外部聲明的,但是在一個引用類型內部,那麼它將會被放到heap上。
下面是另外一個例子:
假如我們有如下一個類:
public class MyInt
{
public int Myvalue;
}
然後下面的方法將被執行:
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
像上次一樣,線程開始執行方法,而且方法的參數已經被放到了線程的Stack上了,如下圖:
然後事情開始變得有意思了。。
因爲MyInt是一個引用類型,它被在Heap上創建,在Stack上通過一個Pointer來引用。
當addFive方法執行完畢後,我們開始清理。。。
此時我們只剩下heap上的孤零零的MyInt對象,此時在Stack上沒有任何引用指向MyInt.
這時候也就是GC發生作用的時候。一旦我們的程序到達了某個內存點,而且我們
需要更多的heap空間,GC就會出來工作。GC將會停止所有的運行線程,找到
heap上所有的不在被main程序引用的對象,並且刪除他們。GC將會重新組織
在heap上剩餘的對象,然後在Stack和Heap上調整這些對象的引用。這個過程
是十分耗費性能的,所以你能意識到當需要些高性能的程序的時候爲什麼需要注意在Stack和Heap上有什麼。
然後,很好,可是那個和我有啥關係?
當我們使用引用類型的時候,我們其實是通過指針和引用類型打交道,而不是那個對象自己。當我們使用值類型的時候,
我們是使用的對象本身,是不是?
如下例子所述:
假如我們執行下述方法:
public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
我們將會得到值3,足夠簡單吧。
然而,假如我們使用之前定義的MyIntClass。
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;
}
public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}
在下一個例子中,我們沒有得到3,因爲x,y指向了heap上的同一個對象。
通過這些,你能夠理解ValueType和ReferenceType的區別了吧。