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