.Net GC

什么是GC

GC如其名,就是垃圾收集,當(dāng)然這里僅就內(nèi)存而言。Garbage Collector(垃圾收集器,在不至于混淆的情況下也成為GC)以應(yīng)用程序的root為基礎(chǔ),遍歷應(yīng)用程序在Heap上動(dòng)態(tài)分配的所有對(duì)象[2],通過識(shí)別它們是否被引用來確定哪些對(duì)象是已經(jīng)死亡的、哪些仍需要被使用。已經(jīng)不再被應(yīng)用程序的root或者別的對(duì)象所引用的對(duì)象就是已經(jīng)死亡的對(duì)象,即所謂的垃圾,需要被回收。這就是GC工作的原理。為了實(shí)現(xiàn)這個(gè)原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統(tǒng).NET CLR,Java VM和Rotor都是采用的Mark Sweep算法。

一、Mark-Compact 標(biāo)記壓縮算法

簡(jiǎn)單地把.NET的GC算法看作Mark-Compact算法。階段1: Mark-Sweep 標(biāo)記清除階段,先假設(shè)heap中所有對(duì)象都可以回收,然后找出不能回收的對(duì)象,給這些對(duì)象打上標(biāo)記,最后heap中沒有打標(biāo)記的對(duì)象都是可以被回收的;階段2: Compact 壓縮階段,對(duì)象回收之后heap內(nèi)存空間變得不連續(xù),在heap中移動(dòng)這些對(duì)象,使他們重新從heap基地址開始連續(xù)排列,類似于磁盤空間的碎片整理。


net-mem-02-mark-compact.png

Heap內(nèi)存經(jīng)過回收、壓縮之后,可以繼續(xù)采用前面的heap內(nèi)存分配方法,即僅用一個(gè)指針記錄heap分配的起始地址就可以。主要處理步驟:將線程掛起→確定roots→創(chuàng)建reachable objects graph→對(duì)象回收→heap壓縮→指針修復(fù)。可以這樣理解roots:heap中對(duì)象的引用關(guān)系錯(cuò)綜復(fù)雜(交叉引用、循環(huán)引用),形成復(fù)雜的graph,roots是CLR在heap之外可以找到的各種入口點(diǎn)。

GC搜索roots的地方包括全局對(duì)象、靜態(tài)變量、局部對(duì)象、函數(shù)調(diào)用參數(shù)、當(dāng)前CPU寄存器中的對(duì)象指針(還有finalization queue)等。主要可以歸為2種類型:已經(jīng)初始化了的靜態(tài)變量、線程仍在使用的對(duì)象(stack+CPU register) 。 Reachable objects:指根據(jù)對(duì)象引用關(guān)系,從roots出發(fā)可以到達(dá)的對(duì)象。例如當(dāng)前執(zhí)行函數(shù)的局部變量對(duì)象A是一個(gè)root object,他的成員變量引用了對(duì)象B,則B是一個(gè)reachable object。從roots出發(fā)可以創(chuàng)建reachable objects graph,剩余對(duì)象即為unreachable,可以被回收 。


net-mem-05-reachable-graph.png

指針修復(fù)是因?yàn)閏ompact過程移動(dòng)了heap對(duì)象,對(duì)象地址發(fā)生變化,需要修復(fù)所有引用指針,包括stack、CPU register中的指針以及heap中其他對(duì)象的引用指針。Debug和release執(zhí)行模式之間稍有區(qū)別,release模式下后續(xù)代碼沒有引用的對(duì)象是unreachable的,而debug模式下需要等到當(dāng)前函數(shù)執(zhí)行完畢,這些對(duì)象才會(huì)成為unreachable,目的是為了調(diào)試時(shí)跟蹤局部對(duì)象的內(nèi)容。傳給了COM+的托管對(duì)象也會(huì)成為root,并且具有一個(gè)引用計(jì)數(shù)器以兼容COM+的內(nèi)存管理機(jī)制,引用計(jì)數(shù)器為0時(shí),這些對(duì)象才可能成為被回收對(duì)象。Pinned objects指分配之后不能移動(dòng)位置的對(duì)象,例如傳遞給非托管代碼的對(duì)象(或者使用了fixed關(guān)鍵字),GC在指針修復(fù)時(shí)無(wú)法修改非托管代碼中的引用指針,因此將這些對(duì)象移動(dòng)將發(fā)生異常。pinned objects會(huì)導(dǎo)致heap出現(xiàn)碎片,但大部分情況來說傳給非托管代碼的對(duì)象應(yīng)當(dāng)在GC時(shí)能夠被回收掉

二、 Generational 分代算法

程序可能使用幾百M(fèi)、幾G的內(nèi)存,對(duì)這樣的內(nèi)存區(qū)域進(jìn)行GC操作成本很高,分代算法具備一定統(tǒng)計(jì)學(xué)基礎(chǔ),對(duì)GC的性能改善效果比較明顯。將對(duì)象按照生命周期分成新的、老的,根據(jù)統(tǒng)計(jì)分布規(guī)律所反映的結(jié)果,可以對(duì)新、老區(qū)域采用不同的回收策略和算法,加強(qiáng)對(duì)新區(qū)域的回收處理力度,爭(zhēng)取在較短時(shí)間間隔、較小的內(nèi)存區(qū)域內(nèi),以較低成本將執(zhí)行路徑上大量新近拋棄不再使用的局部對(duì)象及時(shí)回收掉。分代算法的假設(shè)前提條件:
  1、大量新創(chuàng)建的對(duì)象生命周期都比較短,而較老的對(duì)象生命周期會(huì)更長(zhǎng);
  2、對(duì)部分內(nèi)存進(jìn)行回收比基于全部?jī)?nèi)存的回收操作要快;
  3、新創(chuàng)建的對(duì)象之間關(guān)聯(lián)程度通常較強(qiáng)。heap分配的對(duì)象是連續(xù)的,關(guān)聯(lián)度較強(qiáng)有利于提高CPU cache的命中率,.NET將heap分成3個(gè)代齡區(qū)域: Gen 0、Gen 1、Gen 2;


net-mem-06-generation.png

Heap分為3個(gè)代齡區(qū)域,相應(yīng)的GC有3種方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap內(nèi)存達(dá)到閥值,則觸發(fā)0代GC,0代GC后Gen 0中幸存的對(duì)象進(jìn)入Gen1。如果Gen 1的內(nèi)存達(dá)到閥值,則進(jìn)行1代GC,1代GC將Gen 0 heap和Gen 1 heap一起進(jìn)行回收,幸存的對(duì)象進(jìn)入Gen2。

2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比較小,這兩個(gè)代齡加起來總是保持在16M左右;Gen2的大小由應(yīng)用程序確定,可能達(dá)到幾G,因此0代和1代GC的成本非常低,2代GC稱為full GC,通常成本很高。粗略的計(jì)算0代和1代GC應(yīng)當(dāng)能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時(shí),full GC可能需要花費(fèi)幾秒時(shí)間。大致上來講.NET應(yīng)用運(yùn)行期間,2代、1代和0代GC的頻率應(yīng)當(dāng)大致為1:10:100。

三、Finalization Queue和Freachable Queue
  這兩個(gè)隊(duì)列和.NET對(duì)象所提供的Finalize方法有關(guān)。這兩個(gè)隊(duì)列并不用于存儲(chǔ)真正的對(duì)象,而是存儲(chǔ)一組指向?qū)ο蟮闹羔?。?dāng)程序中使用了new操作符在Managed Heap上分配空間時(shí),GC會(huì)對(duì)其進(jìn)行分析,如果該對(duì)象含有Finalize方法則在Finalization Queue中添加一個(gè)指向該對(duì)象的指針。

在GC被啟動(dòng)以后,經(jīng)過Mark階段分辨出哪些是垃圾。再在垃圾中搜索,如果發(fā)現(xiàn)垃圾中有被Finalization Queue中的指針?biāo)赶虻膶?duì)象,則將這個(gè)對(duì)象從垃圾中分離出來,并將指向它的指針移動(dòng)到Freachable Queue中。這個(gè)過程被稱為是對(duì)象的復(fù)生(Resurrection),本來死去的對(duì)象就這樣被救活了。為什么要救活它呢?因?yàn)檫@個(gè)對(duì)象的Finalize方法還沒有被執(zhí)行,所以不能讓它死去。Freachable Queue平時(shí)不做什么事,但是一旦里面被添加了指針之后,它就會(huì)去觸發(fā)所指對(duì)象的Finalize方法執(zhí)行,之后將這個(gè)指針從隊(duì)列中剔除,這是對(duì)象就可以安靜的死去了。

.NET Framework的System.GC類提供了控制Finalize的兩個(gè)方法,ReRegisterForFinalize和SuppressFinalize。前者是請(qǐng)求系統(tǒng)完成對(duì)象的Finalize方法,后者是請(qǐng)求系統(tǒng)不要完成對(duì)象的Finalize方法。ReRegisterForFinalize方法其實(shí)就是將指向?qū)ο蟮闹羔樦匦绿砑拥紽inalization Queue中。這就出現(xiàn)了一個(gè)很有趣的現(xiàn)象,因?yàn)樵贔inalization Queue中的對(duì)象可以復(fù)生,如果在對(duì)象的Finalize方法中調(diào)用ReRegisterForFinalize方法,這樣就形成了一個(gè)在堆上永遠(yuǎn)不會(huì)死去的對(duì)象,像鳳凰涅槃一樣每次死的時(shí)候都可以復(fù)生

.NET的GC機(jī)制有這樣兩個(gè)問題:

首先,GC并不是能釋放所有的資源。它不能自動(dòng)釋放非托管資源。

第二,GC并不是實(shí)時(shí)性的,這將會(huì)造成系統(tǒng)性能上的瓶頸和不確定性。

GC并不是實(shí)時(shí)性的,這會(huì)造成系統(tǒng)性能上的瓶頸和不確定性。所以有了IDisposable接口,IDisposable接口定義了Dispose方法,這個(gè)方法用來供程序員顯式調(diào)用以釋放非托管資源。使用using語(yǔ)句可以簡(jiǎn)化資源管理。

GC注意事項(xiàng):
  
  1、只管理內(nèi)存,非托管資源,如文件句柄,GDI資源,數(shù)據(jù)庫(kù)連接等還需要用戶去管理。

2、循環(huán)引用,網(wǎng)狀結(jié)構(gòu)等的實(shí)現(xiàn)會(huì)變得簡(jiǎn)單。GC的標(biāo)志-壓縮算法能有效的檢測(cè)這些關(guān)系,并將不再被引用的網(wǎng)狀結(jié)構(gòu)整體刪除。

3、GC通過從程序的根對(duì)象開始遍歷來檢測(cè)一個(gè)對(duì)象是否可被其他對(duì)象訪問,而不是用類似于COM中的引用計(jì)數(shù)方法。

4、GC在一個(gè)獨(dú)立的線程中運(yùn)行來刪除不再被引用的內(nèi)存。

5、GC每次運(yùn)行時(shí)會(huì)壓縮托管堆。

6、你必須對(duì)非托管資源的釋放負(fù)責(zé)。可以通過在類型中定義Finalizer來保證資源得到釋放。

7、對(duì)象的Finalizer被執(zhí)行的時(shí)間是在對(duì)象不再被引用后的某個(gè)不確定的時(shí)間。注意并非和C++中一樣在對(duì)象超出聲明周期時(shí)立即執(zhí)行析構(gòu)函數(shù)

8、Finalizer的使用有性能上的代價(jià)。需要Finalization的對(duì)象不會(huì)立即被清除,而需要先執(zhí)行Finalizer.Finalizer,不是在GC執(zhí)行的線程被調(diào)用。GC把每一個(gè)需要執(zhí)行Finalizer的對(duì)象放到一個(gè)隊(duì)列中去,然后啟動(dòng)另一個(gè)線程來執(zhí)行所有這些Finalizer,而GC線程繼續(xù)去刪除其他待回收的對(duì)象。在下一個(gè)GC周期,這些執(zhí)行完Finalizer的對(duì)象的內(nèi)存才會(huì)被回收。

9、.NET GC使用"代"(generations)的概念來優(yōu)化性能。代幫助GC更迅速的識(shí)別那些最可能成為垃圾的對(duì)象。在上次執(zhí)行完垃圾回收后新創(chuàng)建的對(duì)象為第0代對(duì)象。經(jīng)歷了一次GC周期的對(duì)象為第1代對(duì)象。經(jīng)歷了兩次或更多的GC周期的對(duì)象為第2代對(duì)象。代的作用是為了區(qū)分局部變量和需要在應(yīng)用程序生存周期中一直存活的對(duì)象。大部分第0代對(duì)象是局部變量。成員變量和全局變量很快變成第1代對(duì)象并最終成為第2代對(duì)象。

10、GC對(duì)不同代的對(duì)象執(zhí)行不同的檢查策略以優(yōu)化性能。每個(gè)GC周期都會(huì)檢查第0代對(duì)象。大約1/10的GC周期檢查第0代和第1代對(duì)象。大約1/100的GC周期檢查所有的對(duì)象。重新思考Finalization的代價(jià):需要Finalization的對(duì)象可能比不需要Finalization在內(nèi)存中停留額外9個(gè)GC周期。如果此時(shí)它還沒有被Finalize,就變成第2代對(duì)象,從而在內(nèi)存中停留更長(zhǎng)時(shí)間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容