C#中的堆和棧(一)

原文連接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

引言

雖然使用.NET Framework時我們不需要主動關心內存管理和垃圾回收,但爲了保證程序的性能,對內存管理和垃圾回收還是應該有必要的瞭解。從根本上理解內存管理的工作方式,也可以幫助我們理解在項目中使用的每一個變量是如何工作的。在本篇中將帶你一起了解堆和棧的基礎,變量的類型以及他們是如何工作的。

在程序運行的過程中,.NET Framework將對象存儲在兩個空間裏。如果你還不知道,我這就給你介紹一下棧(Stack)和堆(Heap),這倆東西都在幫助我們運行代碼。他們寄存在設備的操作內存中,幷包含我們程序運行所需要的所有信息。

堆和棧的區別

棧主要負責存儲指令,堆主要負責保存對象和數據。

棧就像是一摞盒子,一個壓着一個。每當我們調用一個方法(有時候也叫幀"frame")時,就會在棧的頂部壓入一個指令用來告訴我們的程序該幹什麼。我們只能獲取到最頂端的那個盒子。當我們最頂端的盒子用完以後(方法執行完畢)就把它扔掉,繼續使用早先放進來的頂部的盒子。不同的是,堆是爲了存儲信息,而不關心命令的執行,所以堆中的任何信息都可以被隨時訪問到。在堆中沒有像棧一樣的讀寫限制。堆就像是我們洗好了但是沒有整理的散亂在牀上的衣服,我們可以快速的拿到我們想要的。棧就像是我們在壁櫥中已經摞好的鞋盒,我們必須拿走上面的,才能得到下面的。

(譯者曰:內存中的棧和數據結構的棧的性質是一樣的,都是後進先出)

C# Heap(ing) Vs Stack(ing) In .NET

上圖中的內容不能正確的表示在內存中的運行情況,只是爲了幫助理解堆和棧的區別。

棧是自我維護的,也就是說他們主要關心自己的內存管理。當頂部的盒子已經不再使用以後,就會被取出來。至於堆,則需要考慮垃圾回收(GC),以保持堆的整潔(沒有人希望到處都是的髒衣服,還散發出臭臭的味道)。

堆和棧上都有什麼?

在代碼運行的過程中,我們主要有四種類型的東東被放在堆和棧裏面:值類型(Value Types)、引用類型(Reference Types)、指針(Pointers)、指令(Instructions)

值類型

C#中,下面的所有類型都是值類型,因爲他們都繼承自System.ValueType:

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

引用類型

下面列出的都是引用類型,他們繼承自System.Object:

  • class
  • interface
  • delegate
  • object
  • string

指針

第三種類型是類型的引用(a Reference to a Type),也就是指針。(譯者注:在C#中)我們不會顯示的使用指針,因爲他們被CLR管理。指針和引用類型不同,當我們在說啥啥啥是引用類型的時候,意思是說我們通過指針來訪問它。指針在內存中存儲的內容是其他內存空間的地址。指針空間的開闢和我們在堆棧上開闢其他東東的空間一樣,它的值可以是內存地址,也可以是Null。

C# Heap(ing) Vs Stack(ing) In .NET

指令

後面的內容將告訴你,指令是如何工作的...

 

怎樣決定什麼東西該去哪裏?

記住這兩條金科玉律:

  1. 引用類型一定去到堆中。這條夠簡單。
  2. 值類型和指針一般來說在哪裏聲明的就去到哪裏。這條一丟丟的複雜,需要對棧的工作方式有一定的瞭解以後,才能分辨出這些東東是在哪裏聲明的。

棧在代碼運行過程中用來表示各自線程(Thread)的進度。你可以把它理解爲線程的狀態,並且每個線程都有自己的棧。當代碼在執行一個方法時,線程就開始執行那些被編譯出來的在這個方法表中的指令,這會將該方法的參數壓入到線程的棧中。當我們在執行這個方法時,會將遇到的變量也都壓入到棧的頂部。舉個栗子理解起來會更簡單......

運行下面的代碼:

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

接下來看一下在棧的最頂部到底做了什麼。切記,我們看到的這個棧,已經放了其他內容到裏面。

當我們開始執行這個方法的時候,方法的參數首先被壓入到棧內(之後我們會詳細的討論參數傳遞的問題)。

注意:這個方法是不再棧中的,只是作爲參考。

C# Heap(ing) Vs Stack(ing) In .NET

然後,如果方法是第一次被執行,則會進行JIT編譯,編譯出的指令被加入到方法的表中。

C# Heap(ing) Vs Stack(ing) In .NET

在執行方法的時候,我們需要一定的內存留給result變量,它也被分配在棧中。

C# Heap(ing) Vs Stack(ing) In .NET

方法執行結束以後,我們得到被返回的result。

C# Heap(ing) Vs Stack(ing) In .NET

通過將指向可用內存的指針移動到AddFive()方法開始的地方,可以將棧中分配的內存清理掉,然後我們就可以訪問上一個被裝進棧中的方法了。

C# Heap(ing) Vs Stack(ing) In .NET

在這個例子中,變量result被分配到棧中,事實上,所有的在方法內部聲明的值類型的變量,都會被分配在棧中。

現在,再來看看什麼時候值類型會被分配到堆中。還記得這條規則嗎,值類型在哪裏聲明的就到哪裏去。當然,如果一個值類型聲明在方法外部,但是在引用類型的內部,它就會跟着這個引用類型一起被分配到堆中。

再舉個栗子。

假設我們有下面這個MyInt類,因爲它是個class類型,所以它是個引用類型:

public class MyInt  
{            
   public int MyValue;  
} 

並且將執行下面的這個方法:

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

跟前面一樣,線程在執行這個方法的時候,會把它的參數也壓入到棧中。

C# Heap(ing) Vs Stack(ing) In .NET

見證奇蹟的時刻...

因爲MyInt是一個引用類型,被分配在堆中,並且被一個棧中的指針所引用。

C# Heap(ing) Vs Stack(ing) In .NET

像第一個例子中一樣,當AddFive方法執行完畢以後,我們執行清理...

C# Heap(ing) Vs Stack(ing) In .NET

之後再也沒有對MyInt的引用了,它成了堆中的孤兒。

C# Heap(ing) Vs Stack(ing) In .NET

這時候就輪到GC表演了。當我們的程序達到一定的內存上限時,GC就會閃亮登場。GC會將所有的線程掛起(A FULL STOP),然後查找堆中的所有已經沒用的對象,並且刪除他們。然後GC會對堆中剩下的對象進行整理並且修改引用到這些對象的指針,不管是堆中的還是棧中的。你可以想象,這會造成巨大的內存開銷。這也就時爲什麼對於編寫高性能的代碼來說,關注堆棧中的內容這麼重要了。

(譯者注:GC的詳解看這裏

好吧,太棒了,但這對我有什麼影響呢?

好問題!

當我們在使用引用類型時,我們是通過指針來完成的,而不是這個對象本身。當我們在使用值類型的時候,我們使用的是這個對象本身。這樣說也許還不夠清楚。

我們再來舉個栗子。

假設我們執行下面的方法:

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

C# Heap(ing) Vs Stack(ing) In .NET

在後面的栗子裏,我們沒有得到預期的3。因爲變量x和y的指針都指向了堆中的同一個對象。

public int ReturnValue2()  
{  
      MyInt x;  
      x.MyValue = 3;  
      MyInt y;  
      y = x;                  
      y.MyValue = 4;  
      return x.MyValue;  
} 

C# Heap(ing) Vs Stack(ing) In .NET

希望這能夠幫助你對值類型和引用類型的本質區別有更好的理解,並且對C#中的指針有基本的理解。在本系列的下一節中,我們將進一步討論內存的管理,並具體討論方法的參數問題。

Happy coding.

 

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