再回首:值類型和引用類型

前言

關於值類型和引用類型,這又是一個十分沉重的話題。程序員

通常人都知道:數據結構

一、C#中又兩大數據類型,即:值類型和引用類型。性能

二、值類型存在在棧(又稱「堆棧」)上,引用類型存儲在堆上。大數據

三、值類型轉換爲引用類型會發生「裝箱」,引用類型轉換爲值類型會發生「拆箱」;裝箱和拆箱過程會比較耗費資源(.NET後來提供了泛型來優化這一個過程)。優化

以上這些觀點,首先是有錯誤的,其次是很膚淺的。編碼

 

這裏的堆和這裏的棧

 這裏的堆和棧並非直接等於數據結構中的堆棧。堆和棧都是內存中的空間,在C#世界中,內存不止這兩個角色:在C#中,內存分紅5個區,即:spa

一、堆操作系統

二、棧線程

三、自由存儲區、全局/靜態存儲區指針

四、常量存儲區

衆所周知,在32位操做系統中分配給每一個進程的最大內存是2G(總共4G,系統隱藏了2G,若是是企業用戶則分配爲3G隱藏1G)。而咱們從任務管理器中很容易看出咱們會有多個進程同時發生,這樣4G*n 經常超出咱們內存條容量大小。因此:這裏的內存,並非指內存條的內存容量,而是虛擬內存(硬盤存儲)

 

堆、棧(堆棧)是作什麼的

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

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

 

 

 

如何決定放哪兒?

 

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

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

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

下面是一個方法(Method):

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

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

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


 

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

 

當方法執行時,咱們須要在棧上爲「result」變量分配一些內存,

 

方法執行完成,而後方法的結果被返回。

 

經過將棧指針指向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會從新組織堆中全部剩下的對象來節省空間,並調整棧和堆中全部與這些對象相關的指針。你確定會想到這個過程很是耗費性能,因此這時你就會知道爲何咱們須要如此重視棧和堆裏有些什麼,特別是在須要編寫高性能的代碼時。

內存中是如何存放的?

 咱們仍是從這下面這一張圖片開始提及吧:

高、底地址,咱們姑且認爲沒用實際意義,就像給內存空間編號(內存中按一字節即8位位一單元,依次編號)

下面舉兩個例子來講明:加入咱們的搞地址開始位置編號是100000,低地址開始位置編號是100

 一、

當前的堆棧指針爲100000,這代表它的下一個自由存儲空間從99999開始,當咱們在C#中聲明一個int類型的變量A,由於int類型是四個字節,因此它將分配在99996到99999這個存儲單元中。若是咱們接着聲明double變量B(8字節),該變量將分配在99988到99995這個存儲單元。 若是代碼運行到他們的做用域以外,這時候A和B兩個變量都將被刪除,此時的順序正好相反,先刪除變量B,同時堆棧指針會遞增8,也就是從新指向到99996這個位置;接下來刪除變量A,堆棧指針從新指向10000。若是兩個變量是同時聲明的。如int A,B,此時咱們並不知道A和B的分配順序,可是編譯器會確保他們的刪除順序正好和分配順序相反。

 二、

 

瞭解堆棧上的分配方式以後,很明顯,它的性能至關高,同時咱們也發現了它的一個缺點:變量的生存期必須嵌套。這對於某些狀況來講是沒法接受的,有時候咱們須要存儲一些數據而且在方法退出後仍然能保證這部分數據是可使用的。爲此,虛擬內存另外分配了一部分區域,咱們稱之爲託管堆。託管堆和傳統的堆很大的一個不一樣點在於,託管堆在垃圾收集器的控制下進行工做。引用類型就分配在託管堆上,下面咱們來看看引用類型的分配過程。  

假設咱們須要聲明一個Person類並對它進行實例化。 Person p = new Person(); 首先, 系統會在堆棧上給p這個變量在堆棧上分配存儲空間,固然它只是一個引用而已,用來存放Person實例在託管堆上的位置,並無存放真正的Person實例。由於它僅僅是存放一個地址(一個整數值),因此它將在堆棧上佔據4個字節的空間。接下來Person實例將會被存放在託管堆上。和堆棧不一樣,託管堆是由下往上分配的,假設這個實例須要佔據10個字節,假設託管堆上的地址爲100,那麼它將分配在100到109這個存儲單元。 須要注意的是,這個分配和實例的大小有關,若是實例小於85000字節,它會被分配在託管堆。若是超過了85000字節,它將被分配在LOH上 。

因而可知,這個分配過程比值類型的分配方式更爲複雜,所以也就不可避免的有性能方面的損耗。這也是爲何對於小數據量的數據結構咱們更願意使用結構而不是類

 

 

一輩子二,二生萬物

「一」是object,「二」是值類型和引用類型,「萬物」就是C#程序員的各類代碼和程序。

相關文章
相關標籤/搜索