C#中堆棧(Stack)和堆(Heap)的區別——第一部分

即使在.NET框架,我們也不必主動擔心內存管理和垃圾回收(GC),但仍必須牢記內存管理和垃圾回收,以優化應用程序的性能。另外,對內存管理的工作原理有基本的瞭解將有助於解釋我們在編寫的每個程序中使用的變量的行為。在本文中,我將介紹堆棧(Stack)和堆(Heap)的基礎知識,變量的類型以及為什麼某些變量會如此工作。

當您的代碼執行時,.NET框架有兩個位置將項目存儲在內存中。如果您還沒有見過面,請允許我向您介紹堆棧(Stack)和堆(Heap)。堆棧(Stack)和堆(Heap)都可以幫助我們運行代碼。它們駐留在我們機器上的操作內存中,幷包含我們需要的所有信息。


堆棧(Stack) vs. 堆(Heap):有什麼區別?

堆棧(Stack)或多或少地負責跟蹤代碼中正在執行的內容(或所謂的“調用”)。堆(Heap)或多或少負責跟蹤我們的對象(我們的數據,好吧...大部分-我們稍後再討論)。

可以將“堆棧(Stack)”視為一系列堆疊在一起的箱子。每次調用方法(稱為框架)時,我們都要將另一個方框堆疊在上面,以便跟蹤應用程序中發生的事情。我們只能使用堆棧(Stack)頂部框中的內容。當我們完成了頂部框(方法已完成執行)後,我們將其丟棄並繼續使用堆棧頂部的上一個框中的內容。堆(Heap)是類似的,除了它的用途是保存信息(大部分時間不跟蹤執行情況),以便可以隨時訪問堆(Heap)中的任何內容。使用堆(Heap),就可以像在堆棧(Stack)中那樣沒有任何限制的訪問任何內容。堆(Heap)就像我們床上的乾淨衣物堆一樣,我們還沒有花時間將它們收起來-我們可以迅速拿起我們需要的東西。堆棧(Stack)就像壁櫥裡的鞋盒一樣,我們必須把最上面的那個拿下來,才能拿到最下面的那個。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

上面的圖片雖然不能真正表示內存中發生的事情,但可以幫助我們將堆棧(Stack)與堆(Heap)區分開。

堆棧(Stack)是自我維護的,這意味著它基本上負責自己的內存管理。如果不再使用頂部框,則將其丟棄。另一方面,堆(Heap)必須使用垃圾收集(GC),以便保持堆(Heap)乾淨。


堆棧和堆上發生了什麼?

我們將在執行代碼時將四種主要類型的東西放入堆棧(Stack)和堆(Heap)中:值類型,引用類型,指針和指令。


值類型

在C#中,使用以下類型聲明列表聲明的所有“事物”均為值類型(因為它們來自System.ValueType):

  • bool
  • byte
  • char
  • decimal
  • double
  • enum
  • float
  • int
  • long
  • sbyte
  • short
  • struct
  • uint
  • ulong
  • ushort


引用類型

用此列表中的類型聲明的所有“事物”都是引用類型(並且繼承自System.Object ...,當然,除了對象是System.Object對象之外):

  • class
  • interface
  • delegate
  • object
  • string


指針

要放在我們的內存管理方案中的“事物”的第三種類型是對類型的引用。引用通常稱為指針。我們沒有明確使用指針,它們是由公共語言運行時(CLR)管理的。指針(或引用)與引用類型的不同之處在於,當我們說某種東西是引用類型時,意味著我們可以通過指針訪問它。指針是內存中的一大塊空間,指向內存中的另一個空間。就像我們放入堆棧(Stack)和堆(Heap)中的其他任何東西一樣,指針佔用空間,其值可以是內存地址或null。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分


指令

您將在本文後面看到“指令”的工作方式...


如何決定去哪裡?(Huh?)

好的,最後一件事,我們將介紹有趣的東西。

這是我們的兩個黃金法則:

  • 引用類型總是放在堆(Heap)上-很簡單,對吧?
  • 值類型和指針總是放在聲明它們的地方。這稍微複雜一點,並且需要了解堆棧(Stack)如何工作,便於理解“事物”被聲明的位置。

正如我們前面提到的,堆棧(Stack)負責跟蹤代碼執行過程中每個線程的位置(或所謂的)。您可以將其視為線程“狀態”,並且每個線程都有自己的堆棧(Stack)。當我們的代碼調用執行一個方法時,線程開始執行已經被JIT編譯並存在於方法表中的指令,它還將方法的參數放在線程堆棧(Stack)中。然後,當我們遍歷代碼並在方法中遇到變量時,將它們放置在堆棧的頂部。通過示例最容易理解...

採取以下方法。


<code>public int AddFive(int pValue)  
{
int result;
result = pValue + 5;
return result;
}
/<code>

這是堆棧(Stack)頂部的情況。請記住,我們正在查看的內容位於已存在於堆棧(Stack)中的許多其他項之上:

一旦開始執行executin ghte方法,該方法的參數就會放在堆棧(Stack)上(稍後我們將詳細討論傳遞參數)。


注意

該方法不存在於堆棧中,僅作參考說明。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

接下來,將控制(執行該方法的線程)傳遞給位於我們類型的方法表中的AddFive()方法的指令,如果這是我們第一次點擊該方法,則將執行JIT編譯。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

該方法執行時,我們需要一些用於“結果”變量的內存,並將其分配在堆棧(Stack)上。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

該方法完成執行並返回我們的結果。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

通過將指針移到AddFive()開始的可用內存地址來清理堆棧上分配的所有內存,然後我們轉到堆棧上的上一個方法(此處未顯示)。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

在此示例中,我們的“結果”變量被放置在堆棧(Stack)上。事實上,每次在方法主體中聲明值類型時,它將被放置在堆棧(Stack)中。

現在,值類型有時也放置在堆上。還記得規則,值類型總是去聲明它們的地方嗎?好吧,如果在方法外部但在引用類型內部聲明瞭值類型,則將其放置在堆的引用類型內。

這是另一個例子。

如果我們具有以下MyInt類(因為它是一個類,所以是引用類型):


<code>Public MyInt   
{
public int MyValue;
}
/<code>

並且正在執行以下方法:


<code>public MyInt AddFive(int pValue)  
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
/<code>

與之前一樣,線程開始執行該方法,並且其參數被放置在該線程的堆棧(Stack)中。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

現在是時候變得有趣了...

由於MyInt是引用類型,因此將其放置在堆(Heap)上並由堆棧(Stack)上的指針引用。

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

在AddFive()完成執行之後(如第一個示例一樣),我們正在清理...

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

我們在堆(Heap)中只剩下一個孤立的MyInt(堆棧中不再有任何類指向MyInt)!

C#中堆棧(Stack)和堆(Heap)的區別——第一部分

這就是垃圾收集(GC)發揮作用的地方。一旦我們的程序達到一定的內存閾值並且我們需要更多的堆(Heap)空間,我們的GC將啟動。GC將停止所有正在運行的線程(FULL STOP),在堆中找到主程序未訪問的所有對象並將其刪除。然後,GC將重新組織堆中剩餘的所有對象以騰出空間,並調整所有指針指向堆棧(Stack)和堆(Heap)中的這些對象。可以想象,這在性能方面可能是非常昂貴的,因此現在您可以瞭解為什麼在嘗試編寫高性能代碼時,注意堆棧(Stack)和堆(Heap)中的內容很重要。

好的,那太好了,但這對我有何影響?

**好問題。 **

當我們使用引用類型時,我們正在處理類型的指針,而不是事物本身。當我們使用值類型時,我們使用的是事物本身。像泥一樣清澈,對不對?

再次,這是最佳示例。

如果我們執行以下方法:


<code>public int ReturnValue()   

{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
/<code>

我們將獲得值3。足夠簡單,對嗎?

但是,如果我們以前使用的是MyInt類


<code>Public class MyInt   
{
public int MyValue;
}
/<code>

並且我們正在執行以下方法:


<code>public int ReturnValue2()  
{
MyInt x = new MyInt();
x.MyValue = 3;
MyInt y = new MyInt();
y = x;
y.MyValue = 4;
return x.MyValue;
}
/<code>

我們得到什麼?... 4!

為什麼?... x.MyValue如何變成4?...看一下我們在做什麼,看是否有意義:

在第一個示例中,一切按計劃進行:


<code>public int ReturnValue()  
{
int x = 3;
int y = x;
y = 4;
return x;
}
/<code>
C#中堆棧(Stack)和堆(Heap)的區別——第一部分

在下一個示例中,我們沒有得到“ 3”,因為變量“ x”和“ y”都指向堆中的同一對象。


<code>public int ReturnValue2()  
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}
/<code>
C#中堆棧(Stack)和堆(Heap)的區別——第一部分

希望這可以使您更好地理解C#中“值類型”和“引用類型”變量之間的基本區別,以及對什麼是指針以及何時使用它的基本理解。在本系列的下一部分中,我們將進一步研究內存管理,並專門討論方法參數。


分享到:


相關文章: