Please enable Javascript to view the contents

CLR、記憶體管理與資料型別

 ·   ·  ☕ 7 分鐘

最近在用 struct 時一直有些疑慮,決定還是將這部分好好弄清楚,所以就整理了這篇筆記,記錄 .NET「通用語言運行庫」(CLR;Common Language Runtime)如何管理和配置記憶體資源,來存取程式運行中用到的實值型別(value types)或參考型別(reference types)變數資料。

.NET 和 CLR

簡單而言,CLR 就是讓 .NET 應用程式可以運作的虛擬機器。

不同 .NET 語言的來源程式碼,被各自的編譯器編譯後,都會編譯成 CIL(通用中間語言)程式碼,儲存在二進制檔案(.exe/.dll)中。當二進制檔案被執行時,CLR 才會將 CIL 即時編譯成適用於該系統環境的機器程式碼;同時 CLR 也管理和作業系統溝通與安全性問題等諸多事項,而記憶體管理就是其中一項。

clr

因為 .NET 程式需要依靠 CLR 來運行,是受 CLR 所管控,所以也將這些稱為受控程式碼(managed code);但如果是 C/C++ 程式,所有和系統環境的溝通都是由程式碼自己處理,故稱為非受控程式碼(unmanaged code)。

記憶體管理

.NET 程式執行時,CLR 會保留一塊連續的記憶體空間供程式使用(不受其他程式干擾),這個記憶體空間將劃分為多個區域:High Frequency HeapStackGC HeapLarge Object Heap 等,一般 HeapGC HeapLarge Object Heap

High Frequency Heap

  1. 存放型別物件(Type object):含各型別之靜態成員方法表(Method table;存放靜態方法、虛擬方法、一般方法的指標)等。
  2. 程式執行期間一直佔用記憶體空間,不會釋出。

程式載入後,每一次遇到遇到新的靜態成員或方法(或者所屬類別的 instance 第一次建立前),就會配置一個區塊存放此型別物件。這在程式執行期間只會配置這麼一次,之後就一直占用同樣的記憶體位址,直到程式結束才會釋出;當靜態成員或方法多時,會占用比較多的記憶體空間。

Stack(堆疊)

  1. 存放區域變數,如果變數為實值型別,其也存放在 Stack 中,如果變數為參考型別,其指標(Pointer;Heap 記憶體位址)存放在 Stack 中。
  2. 變數宣告後開始占用記憶體,離開變數可見範圍完成生命週期後,記憶體空間自動回收。

Stack 是以「後進先出」的陣列結構來存取資料。隨著程式執行緒方向前進,每宣告一個區域變數時,這個變數就會在 Stack 中「堆疊」,而在離開函式變數不再使用後,記憶體空間會自動釋放。所以先宣告的變數的記憶體空間會最後釋放,最後宣告的變數會最先回收(Last In, First Out;LIFO)。儲存在 Stack 的資料其生命週期是可以預測的,不須特意管理記憶體的釋放。

但因為記憶體空間是有限的,程式沒有寫好而造成無限迴圈或無限遞迴時,Stack 空間不足會發生溢位錯誤(StackOverflowException)。

Heap(堆積)- GC Heap & Large Object Heap

  1. 存放參考型別變數(物件本身)和封箱的(Boxed)實值型別物件等。
  2. 當參考型別的物件建立時,開始占用記憶體,直到空間不足時 GC 釋放沒有指標指向的物件。無法預測生命週期。

.NET 中這個區塊由 Garbage Collector(GC;垃圾回收;記憶體回收)來提供自動記憶體管理服務。自動記憶體管理(Automatic Memory Management)可以避免一些常見的問題,例如忘記釋放不再使用的物件而造成記憶體流失(Memory Leak,也就是 Stack 中的變數和指標已經移除了,但 Heap 中變數對應的物件沒有被釋放),或嘗試存取已經被釋放的物件而造成錯誤等。

當第一個參考型別的實例建立(new)時,GC 會依據所需的記憶體大小在 GC Heap 根位址(base address)上開闢一個空間存放物件,接下來下一個物件的存放位置會與上一個相鄰,下一個再與上一個相鄰…,依照這個原則依序存放直到沒有足夠的空間為止。

當 GC 估計記憶體空間不足時,就會將不再使用、沒有指標指向的物件回收,釋放空間,然後將具有指標的物件重新排序壓縮(以複製刪除方式),將可用的空間挪到一起,供後續新增的物件使用。程式中也可以使用 GC.Collect() 建議 GC 進行回收(不代表 GC 會馬上處理),但通常不需要這麼做。

除了 GC Heap 外,另有一個區塊用來存放大型物件(Large Object Heap),在這個區塊中,一樣是由 GC 管控記憶體配置,但是當大型物件回收後,其他的物件不會重新排序,以避免大型物件的搬移降低效能。

當程式建立大型陣列或資料集合,CLR 無法為他們配置足夠的連續記憶體空間時,會產生 OutOfMemoryException 例外狀況。更多可能導致 OutOfMemoryException 的原因可以參考微軟文件

Stack vs Heap

項目StackHeap
特點靜態記憶體配置;記憶體配置方式具連續性、可預測動態記憶體配置;依使用者需求配置記憶體,空間上不需具連續性
結構特性記憶體陣列;後進先出(LIFO)的資料結構記憶體區塊,依需求分割儲存各種資料物件,物件存取不存在順序關係
比喻疊盤子,最後放上去的會最先拿起來

plates

相片牆,照片可任意拿取或重新排列

photowall

儲存的資料實值型別,和參考型別的指標參考型別的值和封箱的實值型別變數等
記憶體配置速度較慢
可否改變儲存長度不能可以
存取性質不能跨執行緒存取可跨執行緒存取
何時釋放空間(生命週期)區域變數離開存取範圍後沒有指標指向此物件後,由 GC 判斷 Heap 使用需求來規劃清除時機
例外狀況StackOverflowExceptionOutOfMemoryException

圖解資料型別與記憶體配置

原則:實值型別的值會與對應的變數或成員名稱存放相同記憶體位置,參考型別的值會存在 Heap 中,儲存其對應變數或成員名稱的地方會有這個值的指標。

以下共用的 struct 和 class 程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct MyStruct
{
    public int Param { get; set; }
    public MyClass InnerClass { get; set; }
}

class MyClass
{
    public int Param { get; set; }
    public MyStruct InnerStruct { get; set; }
}

1. 實值型別(簡單型別)區域變數

1
2
3
4
5
6
7
8
9
int a;
a = 10;
int b = a;
int c = Add(a, b);

public int Add(int x, int y)
{
    return x + y;
}

valuetype

2. 參考型別(類別)區域變數

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
MyClass myclass1 = new MyClass() { Param = 10 };
MyClass myclass2 = myclass1;
BeNull(myclass1);
ModifyClassParamTo50(myclass2);

public void BeNull(MyClass s)
{
    s = null;  //不會影響原本的物件的指標
}

public void ModifyClassParamTo50(MyClass s)
{
    s.Param = 50;
}

valuetype

3. 參考型別(字串)區域變數

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
string s1 = "ABC";
string s2 = s1;
s2 = "XYZ";
string s3 = StringConcatenate(s1, s2);

public string StringConcatenate(string x, string y)
{
    x += "@@";
    return x + y;
}

valuetype

4. Struct 區域變數(含參考型別成員)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
MyStruct mystruct1 = new MyStruct() { Param = 10 };
MyStruct mystruct2 = new MyStruct() { Param = 10 };
Console.WriteLine("4-1. mystruct2.Equals(mystruct1):" + mystruct2.Equals(mystruct1));   //True

ModifyStructParamTo50(mystruct1);   //作為引數會複製一份資料給方法使用
Console.WriteLine("4-2. mystruct1.Param = " + mystruct1.Param);   //10

MyClass myclass = new MyClass();
mystruct1.InnerClass = myclass;
Console.WriteLine("4-3. mystruct2.Equals(mystruct1):" + mystruct2.Equals(mystruct1));   //False
mystruct2.InnerClass = myclass;
Console.WriteLine("4-4. mystruct2.Equals(mystruct1):" + mystruct2.Equals(mystruct1));   //True

ModifyClassParamTo50(mystruct2.InnerClass);
Console.WriteLine("4-5. mystruct1.InnerClass.Param = " + mystruct1.InnerClass.Param);   //50
Console.WriteLine("4-6. mystruct2.Equals(mystruct1):" + mystruct2.Equals(mystruct1));   //True
Console.WriteLine("4-7. myclass.Equals(mystruct1.InnerClass):" + myclass.Equals(mystruct1.InnerClass));   //True


public void ModifyStructParamTo50(MyStruct s)
{
    s.Param = 50;  //修改複製來的資料,不影響原本 Struct 的值
}

public void ModifyClassParamTo50(MyClass s)
{
    s.Param = 50;
}

valuetype

5. 參考型別區域變數(含 Struct 成員)

1
2
3
MyClass myClass1 = new MyClass() { Param = 10, InnerStruct = new MyStruct() };
MyClass myClass2 = new MyClass() { Param = 10, InnerStruct = new MyStruct() };
Console.WriteLine("5-1. myClass2.Equals(myClass1):" + myClass2.Equals(myClass1));   //False

valuetype

6. 實值型別封箱 Boxing

1
2
MyStruct myStruct = new MyStruct();
object boxed = myStruct;

valuetype

7. 類別陣列

1
MyClass[] classArray = new MyClass[] { new MyClass(), new MyClass()};

valuetype

8. Struct 陣列

1
MyStruct[] structArray = new MyStruct[] { new MyStruct(), new MyStruct()};

valuetype

總結:Struct 使用時機

和 class 比較,其實 struct 的限制頗多,除了作為實值型別需考量其記憶體的使用外,其他還有 struct 只能實作介面,不能繼承或抽象化,所以只有有限的多型性。

歸納其使用的時機如下:

  1. 小型資料結構,且具有實值語意(例如座標XY)
  2. 類別中希望限定只能唯讀的成員
  3. 作為方法引數,但不希望值本身受方法運算結果影響
  4. 應盡量避免 Boxing / Unboxing
  5. 如果無法確定,就還是用 class
相關連結:
  1. [C#Corner] Stack Vs Heap Memory - C#
  2. [C#Corner] Working With Static In C#
  3. [CODE PROJECT] Static Keyword Demystified
  4. [stack overflow] Where are all the static members stored?
  5. [Microsoft Docs] Automatic Memory Management
  6. [Microsoft Docs] C# 型別系統
  7. [Microsoft Docs] Structs
  8. Struct V.S Class 兩者之間差異
  9. Why is List 15 Times Faster to Allocate than List in C#
  10. [Book] Head First C#, 4th Edition
分享

Zoey
作者
Zoey
內容如有錯誤或建議,歡迎來信(contact@zoeydc.com)