品味類型---值類型與引用類型(上)-內存有理

本文將介紹以下內容:

  • 類型的基本概念 
  • 值類型深入
  • 引用類型深入
  • 值類型與引用類型的比較及應用

 

1. 引言

買了新本本,忙了好幾天系統,終於開始了對值類型和引用類型做個全面的講述了,本系列開篇之時就是因爲想寫這個主題,纔有了寫個系列的想法。所以對值類型和引用類型的分析,是我最想成文的一篇,其原因是過去的學習過程中我就是從這個主題開始,喜歡以IL語言來分析執行,也喜好從底層的過程來深入瞭解。這對我來說,似乎是一件找到了有效提高的方法,所以想寫的衝動就沒有停過,旨在以有效的方式來分享所得。同時,我也認爲,對值類型和引用類型的把握,是理解語言基礎環節的關鍵主題,有必要花力氣來了解和深入。  

2. 一切從內存開始

2.1 基本概念

從上回《第七回:品味類型---從通用類型系統開始》我們知道,CLR支持兩種基本類型:值類型引用類型。因此,還是把MSDN這張經典視圖拿出來做個鋪墊。

 

值類型(Value Type),值類型實例通常分配在線程的堆棧(stack)上,並且不包含任何指向實例數據的指針,因爲變量本身就包含了其實例數據。其在MSDN的定義爲值類型直接包含它們的數據,值類型的實例要麼在堆棧上,要麼內聯在結構中。我們由上圖可知,值類型主要包括簡單類型、結構體類型和枚舉類型等。通常聲明爲以下類型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort等時,該變量即爲值類型。  

引用類型(Reference Type),引用類型實例分配在託管堆(managed heap)上,變量保存了實例數據的內存引用。其在MSDN中的定義爲引用類型存儲對值的內存地址的引用,位於堆上。我們由上圖可知,引用類型可以是自描述類型、指針類型或接口類型。而自描述類型進一步細分成數組和類類型。類類型是則可以是用戶定義的類、裝箱的值類型和委託。通常聲明爲以下類型:class、interface、delegate、object、string以及其他的自定義引用類型時,該變量即爲引用類型。

下面簡單的列出我們類型的進一步細分,數據來自MSDN,爲的是給我們的概念中有清晰的類型概念,這是最基礎也是最必須的內容。

  

2.2 內存深入

2.2.1. 內存機制

那麼.NET的內存分配機制如何呢?

數據在內存中的分配位置,取決於該變量的數據類型。由上可知,值類型通常分配在線程的堆棧上,而引用類型通常分配在託管堆上,由GC來控制其回收。例如,現在有MyStruct和MyClass分別代表一個結構體和一個類,如下:

using System;

public class Test
{
    
static void Main()
    {
        
//定義值類型和引用類型,並完成初始化
        MyStruct myStruct = new MyStruct();
        MyClass myClass 
= new MyClass();
        
        
//定義另一個值類型和引用類型,
        
//以便了解其內存區別
        MyStruct myStruct2 = new MyStruct();
        myStruct2 
= myStruct;
        
        MyClass myClass2 
= new MyClass();
        myClass2 
= myClass;        
    }
}

在上述的過程中,我們分別定義了值類型變量myStruct和引用類型變量myClass,並使用new操作符完成內存分配和初始化操作,此處new的區別可以詳見《第五回:深入淺出關鍵字---把new說透》  的論述,在此不做進一步描述。而我們在此強調的是myStruct和myClass兩個變量在內存分配方面的區別,還是以一個簡明的圖來展示一下:

 

我們知道,每個變量或者程序都有其堆棧,不同的變量不能共有同一個堆棧地址,因此myStruct和myStruct2在堆棧中一定佔用了不同的堆棧地址,儘管經過了變量的傳遞,實際的內存還是分配在不同的地址上,如果我們再對myStruct2變量改變時,顯然不會影響到myStruct的數據。從圖中我們還可以顯而易見的看出,myStruct在堆棧中包含其實例數據,而myClass在堆棧中只是保存了其實例數據的引用地址,實際的數據保存在託管堆中。因此,就有可能不同的變量保存了同一地址的數據引用,當數據從一個引用類型變量傳遞到另一個相同類型的引用類型變量時,傳遞的是其引用地址而不是實際的數據,因此一個變量的改變會影響另一個變量的值。從上面的分析就可以明白的知道這樣一個簡單的道理:值類型和引用類型在內存中的分配區別是決定其應用不同的根本原因,由此我們就可以很容易的解釋爲什麼參數傳遞時,按值傳遞不會改變形參值,而按址傳遞會改變行參的值,道理正在於此。 

對於內存分配的更詳細位置,可以描述如下:

  • 值類型變量做爲局部變量時,該實例將被創建在堆棧上;而如果值類型變量作爲類型的成員變量時,它將作爲類型實例數據的一部分,同該類型的其他字段都保存在託管堆上,這點我們將在接下來的嵌套結構部分來詳細說明。
  • 引用類型變量數據保存在託管堆上,但是根據實例的大小有所區別,如下:如果實例的大小小於85000Byte時,則該實例將創建在GC堆上;而當實例大小大於等於85000byte時,則該實例創建在LOH(Large Object Heap)堆上。

更詳細的分析,我推薦《類型實例的創建位置、託管對象在託管堆上的結構》。

2.2.2. 嵌套結構 

嵌套結構就是在值類型中嵌套定義了引用類型,或者在引用類型變量中嵌套定義了值類型,相信園子中關於這一話題的論述和關注都不是很多。因此我們很有必要發揮一下,在此就順藤摸瓜,從上文對.NET的內存機制着手來理解會水到渠成。

  • 引用類型嵌套值類型

值類型如果嵌套在引用類型時,也就是值類型在內聯的結構中時,其內存分配是什麼樣子呢? 其實很簡單,例如類的私有字段如果爲值類型,那它作爲引用類型實例的一部分,也分配在託管堆上。例如:

public class NestedValueinRef

  
//aInt做爲引用類型的一部分將分配在託管堆上 
  private int aInt;  
  
public NestedValueinRef 
  { 
    
//aChar則分配在該段代碼的線程棧上 
     char achar = 'a'
  } 

其內存分配圖可以表示爲:

  

  •  值類型嵌套引用類型

引用類型嵌套在值類型時,內存的分配情況爲:該引用類型將作爲值類型的成員變量,堆棧上將保存該成員的引用,而成員的實際數據還是保存在託管堆中。例如:

public struct NestedRefinValue
{
    
public MyClass myClass;
    
public NestedRefinValue
    {
        myClass.X 
= 1;
        myClass.Y 
= 2;
    }
}

其內存分配圖可以表示爲:

 

2.2.3. 一個簡單的討論

通過上面的分析,如果我們現在有如下的執行時:

AType[] myType = new AType[10];

試問:如果AType是值類型,則分配了多少內存;而如果AType是引用類型時,又分配了多少內存?

我們的分析如下:根據CRL的內存機制,我們知道如果ATpye爲Int32類型,則表示其元素是值類型,而數組本身爲引用類型,myType將保存指向託管堆中的一塊大小爲4×10byte的內存地址,並且將所有的元素賦值爲0;而如果AType爲自定義的引用類型,則會只做一次內存分配,在線程的堆棧創建了一個指向託管堆的引用,而所有的元素被設置爲null值,表示爲空。 
 

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