網(wǎng)站首頁 編程語言 正文
一、CLR
CLR:即公共語言運行時(Common Language Runtime),是中間語言(IL)的運行時環(huán)境,負責將編譯生成的MSIL編譯成計算機可以識別的機器碼,負責資源管理(內(nèi)存分配和垃圾回收等)。
可能有人會提問:為什么不直接編譯成機器碼,而要先編譯成IL,然后在編譯成機器碼呢?
原因是:計算機的操作系統(tǒng)不同(分為32位和64位),接受的計算機指令也是不同的,在不同的操作系統(tǒng)中就要進行不同的編譯,寫出的代碼在不同的操作系統(tǒng)中要進行不同的修改。中間增加了IL層,不管是什么操作系統(tǒng),編譯生成的IL都是相同的,IL被不同操作系統(tǒng)的CLR編譯成機器碼,最終被計算機執(zhí)行。
JIT:即時編譯器,負責編譯成機器碼。
二、內(nèi)存分配
內(nèi)存分配:指程序運行時,進程占用的內(nèi)存,由CLR負責分配。
值類型:值類型是struct的,例如:int、datetime等。
引用類型:即class,例如:類、接口,string等。
1、棧
棧:即線程棧,先進后出的一種數(shù)據(jù)結構,隨著線程而分配,其順序如下:
看下面的例子:
定義一個結構類型
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)用構造函數(shù)(因為在構造函數(shù)里面可以使用this),這時才執(zhí)行構造函數(shù)。
- c、把地址引用傳給棧上面的變量。
3、復雜類型
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)存分配為什么是這種情況呢?值類型不應該是都分配在棧上面嗎?為什么一個是分配在堆上面,一個是分配在棧上面呢?
_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)存分配如下:
從上面的截圖中可以看出:值類型里面的引用類型的變量分配在棧上,值分配在堆上。
總結:
1、方法的局部變量
根據(jù)變量自身的類型決定,與所在的環(huá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賦值的時候,是在棧上面復制一份student的引用給student2,然后student和student2都是指向堆上面的同一塊內(nèi)存。
輸出student和student2的值:
Console.WriteLine("student的值是:" + student); Console.WriteLine("student2的值是:"+student2);
結果:
從結果可以看出:student和student2的值是一樣的,這也能說明student和student2指向的是同一塊內(nèi)存。
這時修改student2的值:
student2 = "App";
這時在輸出student和student2的值,其結果如下圖所示:
從結果中可以看出: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));
結果:
可能有人會想:按照上面講解的,student和student2應該指向的是不同的內(nèi)存地址,結果應該是false啊,為什么會是true呢?這是因為CLR在分配內(nèi)存的時候,會查找是否有相同的值,如果有相同的值,就重用;如果沒有,這時在重新開辟一塊內(nèi)存。所以修改student2以后,student和student2都是指向同一塊內(nèi)存,結果輸出是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();
結果:
從運行結果可以看出,如果是普通的引用類型,如果修改其他一個實例的值,那么另一個實例的值也會改變。正好與string類型相反。
三、內(nèi)存回收
值類型存放在線程棧上,線程棧是每次調(diào)用都會產(chǎn)生,用完自己就會釋放。
引用類型存放在堆上面,全局共享一個堆,空間有限,所以才需要垃圾回收。
CLR在堆上面是連續(xù)分配內(nèi)存的。
1、C#中的資源分為兩類:
a、托管資源
由CLR管理的存在于托管堆上的稱為托管資源,注意這里有2個關鍵點,第一是由CLR管理,第二存在于托管堆上。托管資源的回收工作是不需要人工干預的,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)對象不再被引用----設置對象=null。
2)對象在銷毀器列表中沒有被標記。
c、垃圾回收發(fā)生時機
1)垃圾回收發(fā)生在new的時候,new一個對象時,會在堆中開辟一塊內(nèi)存,這時會查看內(nèi)存空間是否充足,如果內(nèi)存空間不夠,則進行垃圾回收。
2)程序退出的時候也會進行垃圾回收。
d、垃圾回收期工作原理
GC定期檢查對象是否未被引用,如果對象沒有被引用,則在檢查銷毀器列表。若在銷毀器列表中沒有標記,則立即回收。若在銷毀器列表中有標記,則開啟銷毀器線程,由該線程調(diào)用析構函數(shù),析構函數(shù)執(zhí)行完,刪除銷毀器列表中的標記。
注意:
不建議寫析構函數(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)絡連接和數(shù)據(jù)庫連接)。托管類在封裝對非托管資源的直接或間接引用時,需要制定專門的規(guī)則,確保非托管的資源在回收類的一個實例時會被釋放。
在定義一個類時,可以使用兩種機制來自動釋放非托管的資源。這些機制常常放在一起實現(xiàn),因為每種機制都為問題提供了略為不同的解決方法。這兩種機制是:
a、聲明一個析構函數(shù)(或終結器),作為類的一個成員。
b、在類中實現(xiàn)System.IDisposable接口。
1)、析構函數(shù)或終結器
析構函數(shù)看起來類似于一個方法:與包含的類同名,但有一個前綴波形符號(~)。它沒有返回值,不帶參數(shù),也沒有訪問修飾符。看下面的一個例子:
public class MyClass { ////// 析構函數(shù) /// ~MyClass() { // 要執(zhí)行的代碼 } }
析構函數(shù)存在的問題:
a、由于使用C#時垃圾回收器的工作方式,無法確定C#對象的析構函數(shù)何時執(zhí)行。所以,不能在析構函數(shù)中放置需要在某一時刻運行的代碼,也不應該寄希望于析構函數(shù)會以特定順序對不同類的實例調(diào)用。如果對象占用了寶貴而重要的資源,應盡快釋放這些資源,此時就不能等待垃圾回收器來釋放了。
b、C#析構函數(shù)的實現(xiàn)會延遲對象最終從內(nèi)存中刪除的時間。沒有析構函數(shù)的對象會在垃圾回收器的一次處理中從內(nèi)存中刪除,但有析構函數(shù)的對象需要兩次處理才能銷毀:第一次調(diào)用析構函數(shù)時,沒有刪除對象,第二次調(diào)用才真正刪除對象。
c、運行庫使用一個線程來執(zhí)行所有對象的Finalize()方法。如果頻繁使用析構函數(shù),而且使用它們執(zhí)行長時間的清理任務,對性能的影響就會非常顯著。
注意:
在討論C#中的析構函數(shù)時,在低層的.NET體系結構中,這些函數(shù)稱為終結器(finalizer)。在C#中定義析構函數(shù)時,編譯器發(fā)送給程序集的實際上是Finalize()方法,它不會影響源代碼。C#編譯器在編譯析構函數(shù)時,它會隱式地把析構函數(shù)的代碼編譯為等價于重寫Finalize()方法的代碼,從而確保執(zhí)行父類的Finalize()方法。例如,下面的C#代碼等價于編譯器為~MyClass()析構函數(shù)生成的IL:
protected override void Finalize() { try { // 析構函數(shù)中要執(zhí)行的代碼 } finally { // 調(diào)用父類的Finalize()方法 base.Finalize(); } }
2)、IDisposable接口
在C#中,推薦使用System.IDisposable接口替代析構函數(shù)。IDisposable接口定義了一種模式,該模式為釋放非托管的資源提供了確定的機制,并避免產(chǎn)生析構函數(shù)固有的與垃圾回收器相關的問題。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關鍵字來完成此工作。例如:
using (var people = new People()) { // 要處理的代碼 }
4)、析構函數(shù)和Dispose()的區(qū)別
a、析構函數(shù)
析構函數(shù) 主要是用來釋放非托管資源,等著GC的時候去把非托管資源釋放掉 系統(tǒng)自動執(zhí)行。GC回收的時候,CLR一定調(diào)用的,但是可能有延遲(釋放對象不知道要多久呢)。
b、Dispose()
Dispose() 也是釋放非托管資源的,主動釋放,方法本身是沒有意義的,我們需要在方法里面實現(xiàn)對資源的釋放。GC的時候不會調(diào)用Dispose()方法,而是使用對象時,使用者主動調(diào)用這個方法,去釋放非托管資源。
5)、終結器和IDisposable接口的規(guī)則
a、如果類定義了實現(xiàn)IDisposable的成員(類里面的屬性實現(xiàn)了IDisposable接口),那么該類也應該實現(xiàn)IDisposable接口。
b、實現(xiàn)IDisposable并不意味著也應該實現(xiàn)一個終結器。終結器會帶來額外的開銷,因為它需要創(chuàng)建一個對象,釋放該對象的內(nèi)存,需要GC的額外處理。只在需要時才應該實現(xiàn)終結器,例如。發(fā)布本機資源。要釋放本機資源,就需要終結器。
c、如果實現(xiàn)了終結器,也應該實現(xiàn)IDisposable接口。這樣,本機資源可以早些釋放,而不僅是在GC找出被占用的資源時,才釋放資源。
d、在終結器的實現(xiàn)代碼中,不能訪問已經(jīng)終結的對象。終結器的執(zhí)行順序是沒有保證的。
e、如果所使用的一個對象實現(xiàn)了IDisposable接口,就在不再需要對象時調(diào)用Dispose方法。如果在方法中使用這個對象,using語句比較方便。如果對象是類的一個成員,那么類也應該實現(xiàn)IDisposable接口。
原文鏈接:https://www.cnblogs.com/dotnet261010/p/9248555.html
相關推薦
- 2023-05-08 C++中new和delete匹配使用過程詳解_C 語言
- 2022-06-22 使用docker?compose一鍵部署WordPress博客的方法_docker
- 2022-03-21 C#?WINFORM自定義異常處理方法_C#教程
- 2022-11-23 GoLang?unsafe包詳細講解_Golang
- 2023-04-26 Numpy對于NaN值的判斷方法_python
- 2022-09-06 Python詳解argparse參數(shù)模塊之命令行參數(shù)_python
- 2022-08-25 .NET6環(huán)境下實現(xiàn)MQTT通信及詳細代碼演示_實用技巧
- 2022-06-02 JQuery實現(xiàn)動態(tài)漂浮廣告_jquery
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支