欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解CLR的內(nèi)存分配和回收機制

 更新時間:2022年03月09日 16:08:51   作者:.NET開發(fā)菜鳥  
本文詳細講解了CLR的內(nèi)存分配和回收機制,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下

一、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)文章

最新評論