標籤:
解釋1、棧是編譯期間就分配好的記憶體空間,因此你的代碼中必須就棧的大小有明確的定義;堆是程式運行期間動態分配的記憶體空間,你可以根據程式的運行情況確定要分配的堆記憶體的大小
解釋2、
存放在棧中時要管儲存順序,保持著先進後出的原則,他是一片連續的記憶體域,有系統自動分配和維護。
而堆是無序的,他是一片不連續的記憶體域,有使用者自己來控制和釋放,如果使用者自己不釋放的話,當記憶體達到一定的特定值時,通過記憶體回收行程(GC)來回收。
參考型別總是存放在堆中。
實值型別和指標總是放在它們被聲明的地方。
調用方法:系統先將一段編碼(堆的首部地址)放到棧上,緊接著放置方法的參數。然後代碼執行到方法時,尋找棧中放該堆首部地址的所有參數,並通過堆的首部地址來控制堆。
參考型別:總是放在堆當中。
當我們使用參考型別時,實際上只是在處理該類型的指標。而非參考型別本身,使用實值型別的話則是使用其本身。
解釋3、
線程堆棧:簡稱棧 Stack
託管堆: 簡稱堆 Heap
使用.Net架構開發程式的時候,我們無需關心記憶體配置問題,因為有GC這個大管家給我們料理一切。如果我們寫出如下兩段代碼:
程式碼片段1:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
程式碼片段2:
public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
問題1:你知道程式碼片段1在執行的時候,pValue和result在記憶體中是如何存放,生命週期又如何?程式碼片段2呢?
要想釋疑以上問題,我們就應該對.Net下的棧(Stack)和託管堆(Heap)(簡稱堆)有個清楚認識,本立而道生。如果你想提高程式效能,理解棧和堆,必須的!
本文就從棧和堆,類型變數展開,對我們寫的程式進行庖丁解牛。
C#程式在CLR上啟動並執行時候,記憶體從邏輯上劃分兩大塊:棧,堆。這倆基本元素組成我們C#程式的運行環境。
一,棧 vs 堆:區別?
棧通常儲存著我們代碼執行的步驟,如在程式碼片段1中 AddFive()方法,int pValue變數,int result變數等等。而堆上存放的則多是對象,資料等。(譯者注:忽略編譯器最佳化)我們可以把棧想象成一個接著一個疊放在一起的盒子。當我們使用的時候,每次從最頂部取走一個盒子。棧也是如此,當一個方法(或類型)被調用完成的時候,就從棧頂取走(called a Frame,譯註:調用幀),接著下一個。堆則不然,像是一個倉庫,儲存著我們使用的各種對象等資訊,跟棧不同的是他們被調用完畢不會立即被清理掉。
1,棧與堆
(圖1)
棧記憶體無需我們管理,也不受GC管理。當棧頂元素使用完畢,立馬釋放。而堆則需要GC(Garbage collection:垃圾收集器)清理。
二,什麼元素被分配到棧?什麼被分配到堆?
當我們程式執行的時候,在棧和堆中分配有四種主要的類型:實值型別,參考型別,指標,指令。
實值型別:
在C#中,繼承自System.ValueType的類型被稱為實值型別,主要有以下幾種(CLR2.0中支援類型有增加):
* bool
* byte
* char
* decimal
* double
* enum
* float
* int
* long
* sbyte
* short
* struct
* uint
* ulong
* ushort
參考型別:
以下是參考型別,繼承自System.Object:
* class
* interface
* delegate
* object
* string
指標:
在記憶體區中,指向一個類型的引用,通常被稱為“指標”,它是受CLR( Common Language Runtime:通用語言執行平台)管理,我們不能顯示使用。需要注意的是,一個類型的引用即指標跟參考型別是兩個完全不同的概念。指標在記憶體中佔一塊記憶體區,它本身只代表一個記憶體位址(或者null),它所指向的另一塊記憶體區才是我們真正的資料或者類型。2:
(圖2)
指令:
後文對指令再做介紹。
三,如何分配?
我們先看一下兩個觀點:
觀點1,參考型別總是被分配在堆上。(正確?)
觀點2,實值型別和指標總是分配在被定義的地方,他們不一定被分配到棧上。(這個理解起來有點難度,需要慢慢來)
上文提及的棧(Stack),在程式啟動並執行時候,每個線程(Thread)都會維護一個自己的專屬線程堆棧。
當一個方法被調用的時候,主線程開始在所屬程式集的中繼資料中,尋找被呼叫者法,然後通過JIT即時編譯並把結果(一般是本地CPU指令)放在棧頂。CPU通過匯流排從棧頂取指令,驅動程式以執行下去。
下面我們以執行個體來詳談。
還是我們開篇所列的程式碼片段1:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
當AddFive方法開始執行的時候,方法參數(parameters)則在棧上分配。3:
(圖3)
注意:方法並不在棧中存活,圖示僅供參考。
接著,指令指向AddFive方法內部,如果該方法是第一次執行,首先要進行JIT即時編譯。4:
(圖4)
當方法內部開始執行的時候,變數result被分配在棧上,5:
(圖5)
方法執行完畢,而且方法返回後,6所示:
(圖6)
在方法執行完畢返回後,棧上的地區被清理。7:
(圖7)
以上看出,一個實值型別變數,一般會分配在棧上。那觀點2中所述又做何理解?“實值型別和指標總是分配在被定義的地方,他們不一定被分配到棧上”。
原因就是如果一個實值型別被聲明在一個方法體外並且在一個參考型別中,那它就會在堆上進行分配。
還是程式碼片段2:
public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
當線程開始執行AddFive方法的時候,參數被分配到棧上,8所示:
(圖8)
由於MyInt是一個參考型別,所以它被分配到堆上,並且在棧中產生一個指標(result),9:
(圖9)
AddFive方法執行完畢時的情況10:
(圖10)
棧上記憶體被清理,堆中依然存在,11:
(圖11)
當程式需要更多的堆空間時,GC需要進行垃圾清理工作,暫停所有線程,找出所有不可達到對象,即無被引用的對象,進行清理。並通知棧中的指標重新指向地址排序後的對象。現在我們應該知道,瞭解棧和堆,對我們開發出高效能程式的重要性。當我們使用參考型別的時候,一般是對指標進行的操作而非參考型別對象本身。但是實值型別則操作其本身。
接下來,我們用例子說明這一點。
例1:
public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
執行結果為3,稍作修改:
例2:
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。
我們來分析下原因,其執行個體1的跟以下代碼所起效用一樣:
public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}
12所示,在棧上x和y分別佔用一塊記憶體區,互不干擾。
(圖12)
而例2,與以下代碼所起效用一樣:
public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}
13所示,
(圖13)
棧上的指標x和y指向堆上同一個地區,修改其一必會改變堆上的資料。
C#堆和棧的區別