c#如何用好垃圾回收機制GC
一、為什么需要GC
應(yīng)用程序?qū)Y源操作,通常簡單分為以下幾個步驟:
1、為對應(yīng)的資源分配內(nèi)存
2、初始化內(nèi)存
3、使用資源
4、清理資源
5、釋放內(nèi)存
應(yīng)用程序?qū)Y源(內(nèi)存使用)管理的方式,常見的一般有如下幾種:
1、手動管理:C,C++
2、計數(shù)管理:COM
3、自動管理:.NET,Java,PHP,GO…
但是,手動管理和計數(shù)管理的復(fù)雜性很容易產(chǎn)生以下典型問題:
1.程序員忘記去釋放內(nèi)存
2.應(yīng)用程序訪問已經(jīng)釋放的內(nèi)存
產(chǎn)生的后果很嚴(yán)重,常見的如內(nèi)存泄露、數(shù)據(jù)內(nèi)容亂碼,而且大部分時候,程序的行為會變得怪異而不可預(yù)測,還有Access Violation等。
.NET、Java等給出的解決方案,就是通過自動垃圾回收機制GC進行內(nèi)存管理。這樣,問題1自然得到解決,問題2也沒有存在的基礎(chǔ)。
總結(jié):無法自動化的內(nèi)存管理方式極容易產(chǎn)生bug,影響系統(tǒng)穩(wěn)定性,尤其是線上多服務(wù)器的集群環(huán)境,程序出現(xiàn)執(zhí)行時bug必須定位到某臺服務(wù)器然后dump內(nèi)存再分析bug所在,極其打擊開發(fā)人員編程積極性,而且源源不斷的類似bug讓人厭惡。
二、GC是如何工作的
GC的工作流程主要分為如下幾個步驟:
1、標(biāo)記(Mark)
2、計劃(Plan)
3、清理(Sweep)
4、引用更新(Relocate)
5、壓縮(Compact)
(一)、標(biāo)記
目標(biāo):找出所有引用不為0(live)的實例
方法:找到所有的GC的根結(jié)點(GC Root), 將他們放到隊列里,然后依次遞歸地遍歷所有的根結(jié)點以及引用的所有子節(jié)點和子子節(jié)點,將所有被遍歷到的結(jié)點標(biāo)記成live。弱引用不會被考慮在內(nèi)
(二)、計劃和清理
1、計劃
目標(biāo):判斷是否需要壓縮
方法:遍歷當(dāng)前所有的generation上所有的標(biāo)記(Live),根據(jù)特定算法作出決策
2、清理
目標(biāo):回收所有的free空間
方法:遍歷當(dāng)前所有的generation上所有的標(biāo)記(Live or Dead),把所有處在Live實例中間的內(nèi)存塊加入到可用內(nèi)存鏈表中去
(三)、引用更新和壓縮
1、引用更新
目標(biāo): 將所有引用的地址進行更新
方法:計算出壓縮后每個實例對應(yīng)的新地址,找到所有的GC的根結(jié)點(GC Root), 將他們放到隊列里,然后依次遞歸地遍歷所有的根結(jié)點以及引用的所有子節(jié)點和子子節(jié)點,將所有被遍歷到的結(jié)點中引用的地址進行更新,包括弱引用。
2、壓縮
目標(biāo):減少內(nèi)存碎片
方法:根據(jù)計算出來的新地址,把實例移動到相應(yīng)的位置。
三、GC的根節(jié)點
本文反復(fù)出現(xiàn)的GC的根節(jié)點也即GC Root是個什么東西呢?
每個應(yīng)用程序都包含一組根(root)。每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要么引用托管堆中的一個對象,要么為null。
在應(yīng)用程序中,只要某對象變得不可達,也就是沒有根(root)引用該對象,這個對象就會成為垃圾回收器的目標(biāo)。
用一句簡潔的英文描述就是:GC roots are not objects in themselves but are instead references to objects.而且,Any object referenced by a GC root will automatically survive the next garbage collection.
.NET中可以當(dāng)作GC Root的對象有如下幾種:
1、全局變量
2、靜態(tài)變量
3、棧上的所有局部變量(JIT)
4、棧上傳入的參數(shù)變量
5、寄存器中的變量
注意,只有引用類型的變量才被認(rèn)為是根,值類型的變量永遠不被認(rèn)為是根。只有深刻理解引用類型和值類型的內(nèi)存分配和管理的不同,才能知道為什么root只能是引用類型。
順帶提一下JAVA,在Java中,可以當(dāng)做GC Root的對象有以下幾種:
1、虛擬機(JVM)棧中的引用的對象
2、方法區(qū)中的類靜態(tài)屬性引用的對象
3、方法區(qū)中的常量引用的對象(主要指聲明為final的常量值)
4、本地方法棧中JNI的引用的對象
四、什么時候發(fā)生GC
1、當(dāng)應(yīng)用程序分配新的對象,GC的代的預(yù)算大小已經(jīng)達到閾值,比如GC的第0代已滿
2、代碼主動顯式調(diào)用System.GC.Collect()
3、其他特殊情況,比如,windows報告內(nèi)存不足、CLR卸載AppDomain、CLR關(guān)閉,甚至某些極端情況下系統(tǒng)參數(shù)設(shè)置改變也可能導(dǎo)致GC回收
五、GC中的代
代(Generation)引入的原因主要是為了提高性能(Performance),以避免收集整個堆(Heap)。一個基于代的垃圾回收器做出了如下幾點假設(shè):
1、對象越新,生存期越短
2、對象越老,生存期越長
3、回收堆的一部分,速度快于回收整個堆
.NET的垃圾收集器將對象分為三代(Generation0,Generation1,Generation2)。不同的代里面的內(nèi)容如下:
1、G0 小對象(Size<85000Byte)
2、G1:在GC中幸存下來的G0對象
3、G2:大對象(Size>=85000Byte);在GC中幸存下來的G1對象
object o = new Byte[85000]; //large object Console.WriteLine(GC.GetGeneration(o)); //output is 2,not 0
ps,這里必須知道,CLR要求所有的資源都從托管堆(managed heap)分配,CLR會管理兩種類型的堆,小對象堆(small object heap,SOH)和大對象堆(large object heap,LOH),其中所有大于85000byte的內(nèi)存分配都會在LOH上進行。一個有趣的問題是為什么是85000字節(jié)?
代收集規(guī)則:當(dāng)一個代N被收集以后,在這個代里的幸存下來的對象會被標(biāo)記為N+1代的對象。GC對不同代的對象執(zhí)行不同的檢查策略以優(yōu)化性能。每個GC周期都會檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。
六、謹(jǐn)慎顯式調(diào)用GC
GC的開銷通常很大,而且它的運行具有不確定性,微軟的編程規(guī)范里是強烈建議你不要顯式調(diào)用GC。但你的代碼中還是可以使用framework中GC的某些方法進行手動回收,前提是你必須要深刻理解GC的回收原理,否則手動調(diào)用GC在特定場景下很容易干擾到GC的正?;厥丈踔烈氩豢深A(yù)知的錯誤。
比如如下代碼:
void SomeMethod() { object o1 = new Object(); object o2 = new Object(); o1.ToString(); GC.Collect(); // this forces o2 into Gen1, because it's still referenced o2.ToString(); }
如果沒有GC.Collect(),o1和o2都將在下一次垃圾自動回收中進入Gen0,但是加上GC.Collect(),o2將被標(biāo)記為Gen1,也就是0代回收沒有釋放o2占據(jù)的內(nèi)存
還有的情況是編程不規(guī)范可能導(dǎo)致死鎖,比如流傳很廣的一段代碼:
public class MyClass { private bool isDisposed = false; ~MyClass() { Console.WriteLine("Enter destructor..."); lock (this) //some situation lead to deadlock { if (!isDisposed) { Console.WriteLine("Do Stuff..."); } } } }
通過如下代碼進行調(diào)用:
var instance = new MyClass(); Monitor.Enter(instance); instance = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("instance is gabage collected");
上述代碼將會導(dǎo)致死鎖。原因分析如下:
1、客戶端主線程調(diào)用代碼Monitor.Enter(instance)代碼段lock住了instance實例
2、接著手動執(zhí)行GC回收,主(Finalizer)線程會執(zhí)行MyClass析構(gòu)函數(shù)
3、在MyClass析構(gòu)函數(shù)內(nèi)部,使用了lock (this)代碼,而主(Finalizer)線程還沒有釋放instance(也即這里的this),此時主線程只能等待
雖然嚴(yán)格來說,上述代碼并不是GC的錯,和多線程操作似乎也無關(guān),而是Lock使用不正確造成的。
同時請注意,GC的某些行為在Debug和Release模式下完全不同(Jeffrey Richter在<<CLR Via C#>>舉過一個Timer的例子說明這個問題)。比如上述代碼,在Debug模式下你可能發(fā)現(xiàn)它是正常運行的,而Release模式下則會死鎖。
七、當(dāng)GC遇到多線程
這一段主要參考<<CLR Via C#>>的線程劫持一節(jié)。
前面討論的垃圾回收算法有一個很大的前提就是:只在一個線程運行。而在現(xiàn)實開發(fā)中,經(jīng)常會出現(xiàn)多個線程同時訪問托管堆的情況,或至少會有多個線程同時操作堆中的對象。一個線程引發(fā)垃圾回收時,其它線程絕對不能訪問任何線程,因為垃圾回收器可能移動這些對象,更改它們的內(nèi)存位置。CLR想要進行垃圾回收時,會立即掛起執(zhí)行托管代碼中的所有線程,正在執(zhí)行非托管代碼的線程不會掛起。然后,CLR檢查每個線程的指令指針,判斷線程指向到哪里。接著,指令指針與JIT生成的表進行比較,判斷線程正在執(zhí)行什么代碼。
如果線程的指令指針恰好在一個表中標(biāo)記好的偏移位置,就說明該線程抵達了一個安全點。線程可在安全點安全地掛起,直至垃圾回收結(jié)束。如果線程指令指針不在表中標(biāo)記的偏移位置,則表明該線程不在安全點,CLR也就不會開始垃圾回收。在這種情況下,CLR就會劫持該線程。也就是說,CLR會修改該線程棧,使該線程指向一個CLR內(nèi)部的一個特殊函數(shù)。然后,線程恢復(fù)執(zhí)行。當(dāng)前的方法執(zhí)行完后,他就會執(zhí)行這個特殊函數(shù),這個特殊函數(shù)會將該線程安全地掛起。然而,線程有時長時間執(zhí)行當(dāng)前所在方法。所以,當(dāng)線程恢復(fù)執(zhí)行后,大約有250毫秒的時間嘗試劫持線程。過了這個時間,CLR會再次掛起線程,并檢查該線程的指令指針。如果線程已抵達一個安全點,垃圾回收就可以開始了。但是,如果線程還沒有抵達一個安全點,CLR就檢查是否調(diào)用了另一個方法。如果是,CLR再一次修改線程棧,以便從最近執(zhí)行的一個方法返回之后劫持線程。然后,CLR恢復(fù)線程,進行下一次劫持嘗試。所有線程都抵達安全點或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有線程都會恢復(fù),應(yīng)用程序繼續(xù)運行,被劫持的線程返回最初調(diào)用它們的方法。
實際應(yīng)用中,CLR大多數(shù)時候都是通過劫持線程來掛起線程,而不是根據(jù)JIT生成的表來判斷線程是否到達了一個安全點。之所以如此,原因是JIT生成表需要大量內(nèi)存,會增大工作集,進而嚴(yán)重影響性能。
概念敘述到此結(jié)束,手都抄軟了^_^,這書賣的貴和書里面的理論水平一樣有道理。
這里再說一個真實案例。某web應(yīng)用程序中大量使用Task,后在生產(chǎn)環(huán)境發(fā)生莫名其妙的現(xiàn)象,程序時靈時不靈,根據(jù)數(shù)據(jù)庫日志(其實還可以根據(jù)Windows事件跟蹤(ETW)、IIS日志以及dump文件),發(fā)現(xiàn)了Task執(zhí)行過程中有不規(guī)律的未處理的異常,分析后懷疑是CLR垃圾回收導(dǎo)致,當(dāng)然這種情況也只有在高并發(fā)條件下才會暴露出來。
八、開發(fā)中的一些建議和意見
由于GC的代價很大,平時開發(fā)中注意一些良好的編程習(xí)慣有可能對GC有積極正面的影響,否則有可能產(chǎn)生不良效果。
1、盡量不要new很大的object,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內(nèi)存壓縮整理,因為在堆中下移85000字節(jié)或更大的內(nèi)存塊會浪費太多CPU時間
2、不要頻繁的new生命周期很短object,這樣頻繁垃圾回收頻繁壓縮有可能會導(dǎo)致很多內(nèi)存碎片,可以使用設(shè)計良好穩(wěn)定運行的對象池(ObjectPool)技術(shù)來規(guī)避這種問題
3、使用更好的編程技巧,比如更好的算法、更優(yōu)的數(shù)據(jù)結(jié)構(gòu)、更佳的解決策略等等
update:.NET4.5.1及其以上版本已經(jīng)支持壓縮大對象堆,可通過System.Runtime.GCSettings.LargeObjectHeapCompactionMode進行控制實現(xiàn)需要壓縮LOH。可參考這里。
根據(jù)經(jīng)驗,有時候編程思想里的空間換時間真不能亂用,用的不好,不但系統(tǒng)性能不能保證,說不定就會導(dǎo)致內(nèi)存溢出(Out Of Memory),關(guān)于OOM,可以參考我之前寫過的一篇文章有效預(yù)防.NET應(yīng)用程序OOM的經(jīng)驗備忘。
之前在維護一個系統(tǒng)的時候,發(fā)現(xiàn)有很多大數(shù)據(jù)量的處理邏輯,但竟然都沒有批量和分頁處理,隨著數(shù)據(jù)量的不斷膨脹,隱藏的問題會不斷暴露。然后我在重寫的時候,都按照批量多次的思路設(shè)計實現(xiàn),有了多線程、多進程和分布式集群技術(shù),再大的數(shù)據(jù)量也能很好處理,而且性能不會下降,系統(tǒng)也會變得更加穩(wěn)定可靠。
九、GC線程和Finalizer線程
GC在一個獨立的線程中運行來刪除不再被引用的內(nèi)存。
Finalizer則由另一個獨立(高優(yōu)先級CLR)線程來執(zhí)行Finalizer的對象的內(nèi)存回收。
對象的Finalizer被執(zhí)行的時間是在對象不再被引用后的某個不確定的時間,并非和C++中一樣在對象超出生命周期時立即執(zhí)行析構(gòu)函數(shù)。
GC把每一個需要執(zhí)行Finalizer的對象放到一個隊列(從終結(jié)列表移至freachable隊列)中去,然后啟動另一個線程而不是在GC執(zhí)行的線程來執(zhí)行所有這些Finalizer,GC線程繼續(xù)去刪除其他待回收的對象。
在下一個GC周期,這些執(zhí)行完Finalizer的對象的內(nèi)存才會被回收。也就是說一個實現(xiàn)了Finalize方法的對象必需等兩次GC才能被完全釋放。這也表明有Finalize的方法(Object默認(rèn)的不算)的對象會在GC中自動“延長”生存周期。
特別注意:負責(zé)調(diào)用Finalize的線程并不保證各個對象的Finalize的調(diào)用順序,這可能會帶來微妙的依賴性問題(見<<CLR Via C#>>一個有趣的依賴性問題)。
以上就是c#如何用好垃圾回收機制GC的詳細內(nèi)容,更多關(guān)于C# 垃圾回收機制GC的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ItemsControl 數(shù)據(jù)綁定的兩種方式
這篇文章主要介紹了ItemsControl 數(shù)據(jù)綁定的兩種方式,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-03-03c# DataView.ToTable()方法 去除表的重復(fù)項問題
這篇文章主要介紹了c# DataView.ToTable()方法 去除表的重復(fù)項問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12使用C#實現(xiàn)基于TCP和UDP協(xié)議的網(wǎng)絡(luò)通信程序的基本示例
這篇文章主要介紹了使用C#實現(xiàn)基于TCP和UDP協(xié)議的網(wǎng)絡(luò)通信程序的示例,文中分別編寫了基本的服務(wù)器端和客戶端,代碼十分簡單,需要的朋友可以參考下2016-04-04C#實現(xiàn)控制線程池最大數(shù)并發(fā)線程
這篇文章主要介紹了C#實現(xiàn)控制線程池最大數(shù)并發(fā)線程的相關(guān)資料,需要的朋友可以參考下2016-07-07C#使用DateAndTime.DateDiff實現(xiàn)計算年齡
這篇文章主要為大家詳細介紹了C#如何使用DateAndTime.DateDiff實現(xiàn)根據(jù)生日計算年齡,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2024-01-01