淺談C#中堆和棧的區別(附上圖解)

C#中棧是編譯期間就分配好的內存空間,因此你的代碼中必須就棧的大小有明確的定義;堆是程序運行期間動態分配的內存空間,你可以根據程序的運行情況確定要分配的堆內存的大小

線程堆棧:簡稱棧 Stack
託管堆: 簡稱堆 Heap

使用.Net框架開發程序的時候,我們無需關心內存分配問題,因爲有GC這個大管家給我們料理一切。如果我們寫出如下兩段代碼:
代碼段1:

1
2
3
4
5
6
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}


代碼段2:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
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:

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,稍作修改:

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的跟以下代碼所起效用一樣:

1
2
3
4
5
6
7
public int ReturnValue()
{
int x = 3;
int y = x; 
y = 4;
return x;
}


如圖12所示,在棧上x和y分別佔用一塊內存區,互不干擾。

(圖12)

而例2,與以下代碼所起效用一樣:

1
2
3
4
5
6
7
8
9
public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x; 
y.MyValue = 4;
return x.MyValue;
}

如圖13所示,


(圖13)
棧上的指針x和y指向堆上同一個區域,修改其一必會改變堆上的數據。


本文原地址:http://www.jb51.net/article/55306.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章