C# 深入理解堆棧、堆在內存中的實現

博客原文:

http://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

儘管在.NET framework下我們並不需要擔心內存管理和垃圾回收(GarbageCollection),但是我們還是應該瞭解它們,以優化我們的應用程序。同時,還需要具備一些基礎的內存管理工作機制的知識,這樣能夠有助於解釋我們日常程序編寫中的變量的行爲。在本文中我將講解棧和堆的基本知識,變量類型以及爲什麼一些變量能夠按照它們自己的方式工作。

在.NET framework環境下,當我們的代碼執行時,內存中有兩個地方用來存儲這些代碼。假如你不曾瞭解,那就讓我來給你介紹棧(Stack)和堆(Heap)。棧和堆都用來幫助我們運行代碼的,它們駐留在機器內存中,且包含所有代碼執行所需要的信息。

* 棧vs堆:有什麼不同?

棧負責保存我們的代碼執行(或調用)路徑,而堆則負責保存對象(或者說數據,接下來將談到很多關於堆的問題)的路徑。

可以將棧想象成一堆從頂向下堆疊的盒子。當每調用一次方法時,我們將應用程序中所要發生的事情記錄在棧頂的一個盒子中,而我們每次只能夠使用棧頂的那個盒子。當我們棧頂的盒子被使用完之後,或者說方法執行完畢之後,我們將拋開這個盒子然後繼續使用棧頂上的新盒子。堆的工作原理比較相似,但大多數時候堆用作保存信息而非保存執行路徑,因此堆能夠在任意時間被訪問。與棧相比堆沒有任何訪問限制,堆就像牀上的舊衣服,我們並沒有花時間去整理,那是因爲可以隨時找到一件我們需要的衣服,而棧就像儲物櫃裏堆疊的鞋盒,我們只能從最頂層的盒子開始取,直到發現那隻合適的。


以上圖片並不是內存中真實的表現形式,但能夠幫助我們區分棧和堆。

棧是自行維護的,也就是說內存自動維護棧,當棧頂的盒子不再被使用,它將被拋出。相反的,堆需要考慮垃圾回收,垃圾回收用於保持堆的整潔性,沒有人願意看到周圍都是贓衣服,那簡直太臭了!

* 棧和堆裏有些什麼?

當我們的代碼執行的時候,棧和堆中主要放置了四種類型的數據:值類型(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)來管理。指針(或引用)是不同於引用類型的,是因爲當我們說某個事物是一個引用類型時就意味着我們是通過指針來訪問它的。指針是一塊內存空間,而它指向另一個內存空間。就像棧和堆一樣,指針也同樣要佔用內存空間,但它的值是一個內存地址或者爲空。


4.指令:

在後面的文章中你會看到指令是如何工作的...

* 如何決定放哪兒?


這裏有一條黃金規則:

1. 引用類型總是放在堆中。(夠簡單的吧?)

2. 值類型和指針總是放在它們被聲明的地方。(這條稍微複雜點,需要知道棧是如何工作的,然後才能斷定是在哪兒被聲明的。)

就像我們先前提到的,棧是負責保存我們的代碼執行(或調用)時的路徑。當我們的代碼開始調用一個方法時,將放置一段編碼指令(在方法中)到棧上,緊接着放置方法的參數,然後代碼執行到方法中的被“壓棧”至棧頂的變量位置。通過以下例子很容易理解...

下面是一個方法(Method):

          public int AddFive(int pValue)
          {
               int result;
               result = pValue + 5;
               return result;
          }

現在就來看看在棧頂發生了些什麼,記住我們所觀察的棧頂下實際已經壓入了許多別的內容。

首先方法(只包含需要執行的邏輯字節,即執行該方法的指令,而非方法體內的數據)入棧,緊接着是方法的參數入棧。(我們將在後面討論更多的參數傳遞)


接着,控制(即執行方法的線程)被傳遞到堆棧中AddFive()的指令上,


當方法執行時,我們需要在棧上爲“result”變量分配一些內存,


Themethod finishes execution and our result is returned.
方法執行完成,然後方法的結果被返回。


通過將棧指針指向AddFive()方法曾使用的可用的內存地址,所有在棧上的該方法所使用內存都被清空,且程序將自動回到棧上最初的方法調用的位置(在本例中不會看到)。


在這個例子中,我們的"result"變量是被放置在棧上的,事實上,當值類型數據在方法體中被聲明時,它們都是被放置在棧上的。

值類型數據有時也被放置在堆上。記住這條規則--值類型總是放在它們被聲明的地方。好的,如果一個值類型數據在方法體外被聲明,且存在於一個引用類型中,那麼它將被堆中的引用類型所取代。


來看另一個例子:

假如我們有這樣一個MyInt類(它是引用類型因爲它是一個類類型):

         public class MyInt
         {          
             publicint 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會重新組織堆中所有剩下的對象來節省空間,並調整棧和堆中所有與這些對象相關的指針。你肯定會想到這個過程非常耗費性能,所以這時你就會知道爲什麼我們需要如此重視棧和堆裏有些什麼,特別是在需要編寫高性能的代碼時。

Ok...這太棒了, 當它是如何影響我的?

Goodquestion. 

當我們使用引用類型時,我們實際是在處理該類型的指針,而非該類型本身。當我們使用值類型時,我們是在使用值類型本身。聽起來很迷糊吧?

同樣,例子是最好的描述。

假如我們執行以下的方法:

         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;
          }


在第二個例子中,我們沒有得到"3"是因爲變量"x"和"y"都同時指向了堆中相同的對象。
          public intReturnValue2()
          {
               MyInt x;
               x.MyValue = 3;
               MyInt y;
               y =x;                
               y.MyValue = 4;
               return x.MyValue;
          }


希望以上內容能夠使你對C#中的值類型和引用類型的基本區別有一個更好的認識,並且對指針及指針是何時被使用的有一定的基本瞭解。在系列的下一個部分,我們將深入內存管理並專門討論方法參數。

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