詳解CLR的內(nèi)存分配和回收機制
一、CLR
CLR:即公共語言運行時(Common Language Runtime),是中間語言(IL)的運行時環(huán)境,負責(zé)將編譯生成的MSIL編譯成計算機可以識別的機器碼,負責(zé)資源管理(內(nèi)存分配和垃圾回收等)。
可能有人會提問:為什么不直接編譯成機器碼,而要先編譯成IL,然后在編譯成機器碼呢?
原因是:計算機的操作系統(tǒng)不同(分為32位和64位),接受的計算機指令也是不同的,在不同的操作系統(tǒng)中就要進行不同的編譯,寫出的代碼在不同的操作系統(tǒng)中要進行不同的修改。中間增加了IL層,不管是什么操作系統(tǒng),編譯生成的IL都是相同的,IL被不同操作系統(tǒng)的CLR編譯成機器碼,最終被計算機執(zhí)行。
JIT:即時編譯器,負責(zé)編譯成機器碼。
二、內(nèi)存分配
內(nèi)存分配:指程序運行時,進程占用的內(nèi)存,由CLR負責(zé)分配。
值類型:值類型是struct的,例如:int、datetime等。
引用類型:即class,例如:類、接口,string等。
1、棧
棧:即線程棧,先進后出的一種數(shù)據(jù)結(jié)構(gòu),隨著線程而分配,其順序如下:
看下面的例子:
定義一個結(jié)構(gòu)類型
public struct ValuePoint { public int x; public ValuePoint(int x) { this.x = x; } }
在方法里面調(diào)用:
//先聲明變量,沒有初始化 但是我可以正常賦值 跟類不同 ValuePoint valuePoint; valuePoint.x = 123; ValuePoint point = new ValuePoint(); Console.WriteLine(valuePoint.x);
內(nèi)存分配情況如下圖所示:
注意:
(1)、值類型分配在線程棧上面,變量和值都是在線程棧上面。
(2)、值類型可以先聲明變量而不用初始化。
2、堆
堆:對象堆,是進程中獨立劃出來的一塊內(nèi)存,有時一些對象需要長期使用不釋放、對象的重用,這些對象就需要放到堆上。
來看下面的例子:
定義一個類
public class ReferencePoint { public int x; public ReferencePoint(int x) { this.x = x; } }
在代碼中調(diào)用:
ReferencePoint referencePoint = new ReferencePoint(123); Console.WriteLine(referencePoint.x);
其內(nèi)存分配如下:
注意:
(1)、引用類型分配在堆上面,變量在棧上面,值在堆上面。
(2)、引用類型分配內(nèi)存的步驟:
- a、new的時候去對象堆里面開辟一塊內(nèi)存,分配一個內(nèi)存地址。
- b、調(diào)用構(gòu)造函數(shù)(因為在構(gòu)造函數(shù)里面可以使用this),這時才執(zhí)行構(gòu)造函數(shù)。
- c、把地址引用傳給棧上面的變量。
3、復(fù)雜類型
a、引用類型里面嵌套值類型
先看下面引用類型的定義:
public class ReferenceTypeClass { private int _valueTypeField; public ReferenceTypeClass() { _valueTypeField = 0; } public void Method() { int valueTypeLocalVariable = 0; } }
在一個引用類型里面定義了一個值類型的屬性:_valueTypeField和一個值類型的局部變量:valueTypeLocalVariable,那么這兩個值類型是如何進行內(nèi)存分配的呢?其內(nèi)存分配如下:
內(nèi)存分配為什么是這種情況呢?值類型不應(yīng)該是都分配在棧上面嗎?為什么一個是分配在堆上面,一個是分配在棧上面呢?
_valueTypeField分配在堆上面比較好理解,因為引用類型是在堆上面分配了一整塊內(nèi)存,引用類型里面的屬性也是在堆上面分配內(nèi)存。
valueTypeLocalVariable分配在棧上面是因為valueTypeLocalVariable是一個全新的局部變量,調(diào)用方法的時候,會啟用一個線程去調(diào)用,線程棧來調(diào)用方法,然后把局部變量分配到棧上面。
b、值類型里面嵌套引用類型
先來看看值類型的定義:
public struct ValueTypeStruct { private object _referenceTypeField; public ValueTypeStruct(int x) { _referenceTypeField = new object(); } public void Method() { object referenceTypeLocalVariable = new object(); } }
在值類型里面定義了引用類型,其內(nèi)存是如何分配的呢?其內(nèi)存分配如下:
從上面的截圖中可以看出:值類型里面的引用類型的變量分配在棧上,值分配在堆上。
總結(jié):
1、方法的局部變量
根據(jù)變量自身的類型決定,與所在的環(huán)境沒關(guān)系。變量如果是值類型,就分配在棧上。變量如果是引用類型,內(nèi)存地址的引用存放在棧上,值存放在堆上。
2、對象是引用類型
其屬性/字段,都是在堆上分配內(nèi)存。
3、對象是值類型
其屬性/字段由自身的類型決定。屬性/字段是值類型就分配在棧上;屬性/字段是引用類型就分配在堆上。
上面的三種情況可以概括成下面一句話:
引用類型在任何時候都是分配在堆上;值類型任何時候都是分配在棧上,除非值類型是在引用類型里面。
4、String字符串的內(nèi)存分配
首先要明確一點:string是引用類型。
先看看下面的例子:
string student = "大山";//在堆上面開辟一塊兒內(nèi)存 存放“大山” 返還一個引用(student變量)存放在棧上
其內(nèi)存分配如下圖所示:
這時,在聲明一個變量student2,然后用student給student2賦值:
string student2 = student;
這時內(nèi)存是如何分配的呢?其內(nèi)存分配如下:
從上面的截圖中可以看出:student2被student賦值的時候,是在棧上面復(fù)制一份student的引用給student2,然后student和student2都是指向堆上面的同一塊內(nèi)存。
輸出student和student2的值:
Console.WriteLine("student的值是:" + student); Console.WriteLine("student2的值是:"+student2);
結(jié)果:
從結(jié)果可以看出:student和student2的值是一樣的,這也能說明student和student2指向的是同一塊內(nèi)存。
這時修改student2的值:
student2 = "App";
這時在輸出student和student2的值,其結(jié)果如下圖所示:
從結(jié)果中可以看出:student的值保持不變,student2的值變?yōu)锳pp,為什么是這樣呢?這是因為string字符串的不可變性造成的。一個string變量一旦聲明并初始化以后,其在堆上面分配的值就不會改變了。這時修改student2的值,并不會去修改堆上面分配的值,而是重新在堆上面開辟一塊內(nèi)存來存放student2修改后的值。修改后的內(nèi)存分配如下:
在看下面一個例子:
string student = "大山"; string student2 = "App"; student2 = "大山"; Console.WriteLine(object.ReferenceEquals(student,student2));
結(jié)果:
可能有人會想:按照上面講解的,student和student2應(yīng)該指向的是不同的內(nèi)存地址,結(jié)果應(yīng)該是false啊,為什么會是true呢?這是因為CLR在分配內(nèi)存的時候,會查找是否有相同的值,如果有相同的值,就重用;如果沒有,這時在重新開辟一塊內(nèi)存。所以修改student2以后,student和student2都是指向同一塊內(nèi)存,結(jié)果輸出是true。
注意:
這里需要區(qū)分string和其他引用類型的內(nèi)存分配。其他引用類型的情況和string正好相反。看下面的例子
先定義一個Refence類,里面有一個int類型的屬性,類定義如下:
public class Refence { public int Value { get; set; } }
在Main()方法里面調(diào)用:
Refence r1 = new Refence(); r1.Value = 30; Refence r2 = r1; Console.WriteLine($"r2.Value的值:{r2.Value}"); r2.Value = 50; Console.WriteLine($"r1.Value的值:{r1.Value}"); Console.ReadKey();
結(jié)果:
從運行結(jié)果可以看出,如果是普通的引用類型,如果修改其他一個實例的值,那么另一個實例的值也會改變。正好與string類型相反。
三、內(nèi)存回收
值類型存放在線程棧上,線程棧是每次調(diào)用都會產(chǎn)生,用完自己就會釋放。
引用類型存放在堆上面,全局共享一個堆,空間有限,所以才需要垃圾回收。
CLR在堆上面是連續(xù)分配內(nèi)存的。
1、C#中的資源分為兩類:
a、托管資源
由CLR管理的存在于托管堆上的稱為托管資源,注意這里有2個關(guān)鍵點,第一是由CLR管理,第二存在于托管堆上。托管資源的回收工作是不需要人工干預(yù)的,CLR會在合適的時候調(diào)用GC(垃圾回收器)進行回收。
b、非托管資源
非托管資源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI資源, 數(shù)據(jù)庫連接等等資源(這里僅僅列舉出幾個常用的)。這些資源GC是不會自動回收的,需要手動釋放。
2、托管資源
a、垃圾回收期(GC)
定期或在內(nèi)存不夠時,通過銷毀不再需要或不再被引用的對象,來釋放內(nèi)存,是CLR的一個重要組件。
b、垃圾回收器銷毀對象的兩個條件
1)對象不再被引用----設(shè)置對象=null。
2)對象在銷毀器列表中沒有被標記。
c、垃圾回收發(fā)生時機
1)垃圾回收發(fā)生在new的時候,new一個對象時,會在堆中開辟一塊內(nèi)存,這時會查看內(nèi)存空間是否充足,如果內(nèi)存空間不夠,則進行垃圾回收。
2)程序退出的時候也會進行垃圾回收。
d、垃圾回收期工作原理
GC定期檢查對象是否未被引用,如果對象沒有被引用,則在檢查銷毀器列表。若在銷毀器列表中沒有標記,則立即回收。若在銷毀器列表中有標記,則開啟銷毀器線程,由該線程調(diào)用析構(gòu)函數(shù),析構(gòu)函數(shù)執(zhí)行完,刪除銷毀器列表中的標記。
注意:
不建議寫析構(gòu)函數(shù),原因如下:
1)對象即使不用,也會在內(nèi)存中駐留很長一段時間。
2)銷毀器線程為單獨的線程,非常耗費資源。
e、優(yōu)化策略
1)分級策略
a、首次GC前 全部對象都是0級。
b、第一次GC后,還保留的對象叫1級。這時新創(chuàng)建的對象就是0級。
c、垃圾回收時,先查找0級對象,如果空間還不夠,再去找1級對象,這之后,還存在的一級對象就變成2級,0級對象就變成一級對象。
d、垃圾回收時如果0~2級都不夠,那么就內(nèi)存溢出了。
注意:
越是最近分配的,越是會被回收。因為最近分配的都是0級對象,每次垃圾回收時都是先查詢0級對象。
3、非托管資源
上面講的都是針對托管資源的,托管資源會被GC回收,不需要考慮釋放。但是,垃圾回收器不知道如何釋放非托管的資源(例如,文件句柄、網(wǎng)絡(luò)連接和數(shù)據(jù)庫連接)。托管類在封裝對非托管資源的直接或間接引用時,需要制定專門的規(guī)則,確保非托管的資源在回收類的一個實例時會被釋放。
在定義一個類時,可以使用兩種機制來自動釋放非托管的資源。這些機制常常放在一起實現(xiàn),因為每種機制都為問題提供了略為不同的解決方法。這兩種機制是:
a、聲明一個析構(gòu)函數(shù)(或終結(jié)器),作為類的一個成員。
b、在類中實現(xiàn)System.IDisposable接口。
1)、析構(gòu)函數(shù)或終結(jié)器
析構(gòu)函數(shù)看起來類似于一個方法:與包含的類同名,但有一個前綴波形符號(~)。它沒有返回值,不帶參數(shù),也沒有訪問修飾符。看下面的一個例子:
public class MyClass { /// <summary> /// 析構(gòu)函數(shù) /// </summary> ~MyClass() { // 要執(zhí)行的代碼 } }
析構(gòu)函數(shù)存在的問題:
a、由于使用C#時垃圾回收器的工作方式,無法確定C#對象的析構(gòu)函數(shù)何時執(zhí)行。所以,不能在析構(gòu)函數(shù)中放置需要在某一時刻運行的代碼,也不應(yīng)該寄希望于析構(gòu)函數(shù)會以特定順序?qū)Σ煌惖膶嵗{(diào)用。如果對象占用了寶貴而重要的資源,應(yīng)盡快釋放這些資源,此時就不能等待垃圾回收器來釋放了。
b、C#析構(gòu)函數(shù)的實現(xiàn)會延遲對象最終從內(nèi)存中刪除的時間。沒有析構(gòu)函數(shù)的對象會在垃圾回收器的一次處理中從內(nèi)存中刪除,但有析構(gòu)函數(shù)的對象需要兩次處理才能銷毀:第一次調(diào)用析構(gòu)函數(shù)時,沒有刪除對象,第二次調(diào)用才真正刪除對象。
c、運行庫使用一個線程來執(zhí)行所有對象的Finalize()方法。如果頻繁使用析構(gòu)函數(shù),而且使用它們執(zhí)行長時間的清理任務(wù),對性能的影響就會非常顯著。
注意:
在討論C#中的析構(gòu)函數(shù)時,在低層的.NET體系結(jié)構(gòu)中,這些函數(shù)稱為終結(jié)器(finalizer)。在C#中定義析構(gòu)函數(shù)時,編譯器發(fā)送給程序集的實際上是Finalize()方法,它不會影響源代碼。C#編譯器在編譯析構(gòu)函數(shù)時,它會隱式地把析構(gòu)函數(shù)的代碼編譯為等價于重寫Finalize()方法的代碼,從而確保執(zhí)行父類的Finalize()方法。例如,下面的C#代碼等價于編譯器為~MyClass()析構(gòu)函數(shù)生成的IL:
protected override void Finalize() { try { // 析構(gòu)函數(shù)中要執(zhí)行的代碼 } finally { // 調(diào)用父類的Finalize()方法 base.Finalize(); } }
2)、IDisposable接口
在C#中,推薦使用System.IDisposable接口替代析構(gòu)函數(shù)。IDisposable接口定義了一種模式,該模式為釋放非托管的資源提供了確定的機制,并避免產(chǎn)生析構(gòu)函數(shù)固有的與垃圾回收器相關(guān)的問題。IDisposable接口聲明了一個Dispose()方法,它不帶參數(shù),返回void。例如:
public class People : IDisposable { public void Dispose() { this.Dispose(); } }
Dispose()方法的實現(xiàn)代碼顯式地釋放由對象直接使用的所有非托管資源,并在所有也實現(xiàn)了IDisposable接口的封裝對象上調(diào)用Dispose()方法。這樣,Dispose()方法為何時釋放非托管資源提供了精確的控制。
3)、using語句
C#提供了一種語法,可以確保在實現(xiàn)了IDisposable接口的對象的引用超出作用域時,在該對象上自動調(diào)用Dispose()方法。該語法使用了using關(guān)鍵字來完成此工作。例如:
using (var people = new People()) { // 要處理的代碼 }
4)、析構(gòu)函數(shù)和Dispose()的區(qū)別
a、析構(gòu)函數(shù)
析構(gòu)函數(shù) 主要是用來釋放非托管資源,等著GC的時候去把非托管資源釋放掉 系統(tǒng)自動執(zhí)行。GC回收的時候,CLR一定調(diào)用的,但是可能有延遲(釋放對象不知道要多久呢)。
b、Dispose()
Dispose() 也是釋放非托管資源的,主動釋放,方法本身是沒有意義的,我們需要在方法里面實現(xiàn)對資源的釋放。GC的時候不會調(diào)用Dispose()方法,而是使用對象時,使用者主動調(diào)用這個方法,去釋放非托管資源。
5)、終結(jié)器和IDisposable接口的規(guī)則
a、如果類定義了實現(xiàn)IDisposable的成員(類里面的屬性實現(xiàn)了IDisposable接口),那么該類也應(yīng)該實現(xiàn)IDisposable接口。
b、實現(xiàn)IDisposable并不意味著也應(yīng)該實現(xiàn)一個終結(jié)器。終結(jié)器會帶來額外的開銷,因為它需要創(chuàng)建一個對象,釋放該對象的內(nèi)存,需要GC的額外處理。只在需要時才應(yīng)該實現(xiàn)終結(jié)器,例如。發(fā)布本機資源。要釋放本機資源,就需要終結(jié)器。
c、如果實現(xiàn)了終結(jié)器,也應(yīng)該實現(xiàn)IDisposable接口。這樣,本機資源可以早些釋放,而不僅是在GC找出被占用的資源時,才釋放資源。
d、在終結(jié)器的實現(xiàn)代碼中,不能訪問已經(jīng)終結(jié)的對象。終結(jié)器的執(zhí)行順序是沒有保證的。
e、如果所使用的一個對象實現(xiàn)了IDisposable接口,就在不再需要對象時調(diào)用Dispose方法。如果在方法中使用這個對象,using語句比較方便。如果對象是類的一個成員,那么類也應(yīng)該實現(xiàn)IDisposable接口。
到此這篇關(guān)于詳解CLR的內(nèi)存分配和回收機制的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C#調(diào)用C動態(tài)鏈接庫的實現(xiàn)
動態(tài)鏈接庫是不能直接執(zhí)行的,也不能接收消息,它只是一個獨立的文件,本文主要介紹了C#調(diào)用C動態(tài)鏈接庫的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-01-01C#?基于NAudio實現(xiàn)對Wav音頻文件剪切(限PCM格式)
本文主要介紹了C#基于NAudio工具對Wav音頻文件進行剪切,可以將一個音頻文件剪切成多個音頻文件(限PCM格式),感興趣的小伙伴可以學(xué)習(xí)一下2021-11-11C# 利用StringBuilder提升字符串拼接性能的小例子
一個項目中有數(shù)據(jù)圖表呈現(xiàn),數(shù)據(jù)量稍大時顯得很慢,在使用了StringBuilder后效果提升很明顯,下面有例子2013-07-07