面試官問:C#垃圾回收機制了解多少?
? ?首先說bai下C#中的變量類型吧,duC#中有2個變量類zhi型,一種是值類型,一dao種是引用類型,值類型是zhuan在棧上創(chuàng)建shu,這一類型用不到GC,引用類型是在堆中創(chuàng)建,GC主要是在這里管理對象。
GC對每個對象有個引用計數(shù),所有說只要有變量在引用它,計數(shù)器就不為了,一個變量不再引用這個對象,對象的計數(shù)器就減一,知道計數(shù)器為0時,對象就成為內(nèi)存垃圾了(沒有變量引用它),但是此時垃圾并沒有回收。
那什么時候回收呢,是在內(nèi)存占用超過一定限度是,GC才啟動,釋放垃圾資源,說白了就是delete這些對象,將空間歸還給系統(tǒng)。
但是這還沒完,空間釋放后,內(nèi)存空間就不連續(xù)了,所有GC還要趕一件事,就是將空間整理下,將占用的空間連續(xù)話,具體說就是將空間向上推,就是想高地值轉(zhuǎn)存,這樣空間就連續(xù)了,使用也方便了,然后GC就改變應(yīng)用那些對象的變量里地地址,讓他們指向正確的位置,所以說C#中的引用類型就是一種指針,一種動態(tài)改變值的指針。
什么是GC
GC(Garbage Collector)就是垃圾收集器,這里僅就內(nèi)存而言。以應(yīng)用程序的root為基礎(chǔ),遍歷應(yīng)用程序在Heap上動態(tài)分配的所有對象,通過識別它們是否被引用來確定哪些對象是已經(jīng)死亡的、哪些仍需要被使用。已經(jīng)不再被應(yīng)用程序的root或者別的對象所引用的對象就是已經(jīng)死亡的對象,即所謂的垃圾,需要被回收。這就是GC工作的原理。
為了實現(xiàn)這個原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統(tǒng).NET CLR,Java VM都是采用的Mark Sweep算法。
算法工作原理:
垃圾收集器的本質(zhì),就是跟蹤所有被引用到的對象,整理不再被引用的對象,回收相應(yīng)的內(nèi)存。
這聽起來類似于一種叫做“引用計數(shù)(Reference Counting)”的算法,然而這種算法需要遍歷所有對象,并維護它們的引用情況,所以效率較低些,并且在出現(xiàn)“環(huán)引用”時很容易造成內(nèi)存泄露。
所以.Net中采用了一種叫做“標(biāo)記與清除(Mark Sweep)”算法來完成上述任務(wù)。
“標(biāo)記與清除”算法,顧名思義,這種算法有兩個本領(lǐng):
“標(biāo)記”本領(lǐng)——垃圾的識別:從應(yīng)用程序的root出發(fā),利用相互引用關(guān)系,遍歷其在Heap上動態(tài)分配的所有對象,沒有被引用的對象不被標(biāo)記,即成為垃圾;存活的對象被標(biāo)記,即維護成了一張“根-對象可達(dá)圖”。其實,CLR會把對象關(guān)系看做“樹圖”,這樣會加快遍歷對象的速度。
.Net中利用棧來完成檢測并標(biāo)記對象引用,在不斷的入棧與出棧中完成檢測:先在樹圖中選擇一個需要檢測的對象,將該對象的所有引用壓棧,如此反復(fù)直到棧變空為止。棧變空意味著已經(jīng)遍歷了這個局部根能夠到達(dá)的所有對象。樹圖節(jié)點范圍包括局部變量、寄存器、靜態(tài)變量,這些元素都要重復(fù)這個操作。一旦完成,便逐個對象地檢查內(nèi)存,沒有標(biāo)記的對象變成了垃圾。“清除”本領(lǐng)——回收內(nèi)存:啟用壓縮(Compact)算法,對內(nèi)存中存活的對象進行移動,修改它們的指針,使之在內(nèi)存中連續(xù),這樣空閑的內(nèi)存也就連續(xù)了,這就解決了內(nèi)存碎片問題,當(dāng)再次為新對象分配內(nèi)存時,CLR不必在充滿碎片的內(nèi)存中尋找適合新對象的內(nèi)存空間,所以分配速度會大大提高。
但是大對象(large object heap)除外,GC不會移動一個內(nèi)存中巨無霸,因為它知道現(xiàn)在的CPU不便宜。通常,大對象具有很長的生存期,當(dāng)一個大對象在.NET托管堆中產(chǎn)生時,它被分配在堆的一個特殊部分中,移動大對象所帶來的開銷超過了整理這部分堆所能提高的性能。
Compact算法除了會提高再次分配內(nèi)存的速度,如果新分配的對象在堆中位置很緊湊的話,高速緩存的性能將會得到提高,因為一起分配的對象經(jīng)常被一起使用(程序的局部性原理),所以為程序提供一段連續(xù)空白的內(nèi)存空間是很重要的。
簡單地把.NET的GC算法看作Mark-Sweep 算法。
階段1: Mark-Sweep 標(biāo)記清除階段,先假設(shè)heap中所有對象都可以回收,然后找出不能回收的對象,給這些對象打上標(biāo)記,最后heap中沒有打標(biāo)記的對象都是可以被回收的;
階段2: Compact 壓縮階段,對象回收之后heap內(nèi)存空間變得不連續(xù),在heap中移動這些對象,使他們重新從heap基地址開始連續(xù)排列,類似于磁盤空間的碎片整理。

Mark-Sweep 算法.png
Reachable objects:指根據(jù)對象引用關(guān)系,從roots出發(fā)可以到達(dá)的對象。例如當(dāng)前執(zhí)行函數(shù)的局部變量對象A是一個root object,他的成員變量引用了對象B,則B是一個reachable object。從roots出發(fā)可以創(chuàng)建reachable objects graph,剩余對象即為unreachable,可以被回收 。
GC按什么規(guī)則收集垃圾對象--Generational 分代算法?
.NET將heap分成3個代齡區(qū)域: Gen 0、Gen 1、Gen 2;heap分配的對象是連續(xù)的,關(guān)聯(lián)度較強有利于提高CPU cache的命中率。

Generational 分代算法.png
Heap分為3個代齡區(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中幸存的對象進入Gen1。
如果Gen 1的內(nèi)存達(dá)到閥值,則進行1代GC,1代GC將Gen 0 heap和Gen 1 heap一起進行回收,幸存的對象進入Gen2。2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收。
如果GC跑過了,內(nèi)存空間依然不夠用,那么就拋出了OutOfMemoryException異常。
Gen 0和Gen 1比較小,這兩個代齡加起來總是保持在16M左右;Gen2的大小由應(yīng)用程序確定,可能達(dá)到幾G,因此0代和1代GC的成本非常低,2代GC稱為full GC,通常成本很高。
粗略的計算0代和1代GC應(yīng)當(dāng)能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時,full GC可能需要花費幾秒時間。大致上來講.NET應(yīng)用運行期間,2代、1代和0代GC的頻率應(yīng)當(dāng)大致為1:10:100。
該如何釋放非托管資源呢?
既然有了垃圾收集器,為什么還要Dispose方法和析構(gòu)函數(shù)?
因為CLR的緣故,GC只能釋放托管資源,不能釋放非托管資源(數(shù)據(jù)庫鏈接、文件流等)。所以對于非托管資源一般我們會選擇為類實現(xiàn)IDispose接口,寫一個Dispose方法。
讓調(diào)用者手動調(diào)用這個類的Dispose方法(或者用using語句塊來自動調(diào)用Dispose方法),Dispose執(zhí)行時,析構(gòu)函數(shù)和垃圾收集器都還沒有開始處理這個對象的釋放工作。
如果我們不想為一個類實現(xiàn)Dispose方法,而是想讓它自動的釋放非托管資源,那么就要用到析構(gòu)函數(shù)了。
析構(gòu)函數(shù)是由GC調(diào)用的。你無法預(yù)測析構(gòu)函數(shù)何時會被調(diào)用,所以盡量不要在這里操作可能被回收的托管資源,析構(gòu)函數(shù)只用來釋放非托管資源。
GC釋放包含析構(gòu)函數(shù)的對象,需要垃圾處理器調(diào)用倆次,CLR會先讓析構(gòu)函數(shù)執(zhí)行,再收集它占用的內(nèi)存。
關(guān)于如何釋放非托管資源詳情,可以看一下另一篇文章《C#之托管與非托管資源》
什么場景下手動執(zhí)行垃圾收集?
GC什么時候執(zhí)行垃圾收集是一個非常復(fù)雜的算法(策略),大概可以描述成這樣:如果GC發(fā)現(xiàn)上一次收集了很多對象,釋放了很大的內(nèi)存,那么它就會盡快執(zhí)行第二次回收,如果它頻繁的回收,但釋放的內(nèi)存不多,那么它就會減慢回收的頻率。
所以,盡量不要調(diào)用GC.Collect()這樣會破壞GC現(xiàn)有的執(zhí)行策略。除非你對你的應(yīng)用程序內(nèi)存使用情況非常了解,你知道何時會產(chǎn)生大量的垃圾,那么你可以手動干預(yù)垃圾收集器的工作,例如我有一個大對象,我擔(dān)心GC要過很久才會收集他。
GC.Collect() 方法
作用:強制進行垃圾回收。
| 名稱 | 說明 |
|---|---|
| Collect() | 強制對所有代進行即時垃圾回收。 |
| Collect(Int32) | 強制對零代到指定代進行即時垃圾回收 |
| Collect(Int32, GCCollectionMode) | 強制在 GCCollectionMode 值所指定的時間對零代到指定代進行垃圾回收 |
GC注意事項
只管理內(nèi)存,非托管資源,如文件句柄,GDI資源,數(shù)據(jù)庫連接等還需要用戶去管理。
循環(huán)引用,網(wǎng)狀結(jié)構(gòu)等的實現(xiàn)會變得簡單。GC的標(biāo)志-壓縮算法能有效的檢測這些關(guān)系,并將不再被引用的網(wǎng)狀結(jié)構(gòu)整體刪除。
GC通過從程序的根對象開始遍歷來檢測一個對象是否可被其他對象訪問,而不是用類似于COM中的引用計數(shù)方法。
GC在一個獨立的線程中運行來刪除不再被引用的內(nèi)存。
GC每次運行時會壓縮托管堆。
你必須對非托管資源的釋放負(fù)責(zé)??梢酝ㄟ^在類型中定義Finalizer來保證資源得到釋放。
對象的Finalizer被執(zhí)行的時間是在對象不再被引用后的某個不確定的時間。注意并非和C++中一樣在對象超出聲明周期時立即執(zhí)行析構(gòu)函數(shù)
Finalizer的使用有性能上的代價。需要Finalization的對象不會立即被清除,而需要先執(zhí)行Finalizer.Finalizer,不是在GC執(zhí)行的線程被調(diào)用。GC把每一個需要執(zhí)行Finalizer的對象放到一個隊列中去,然后啟動另一個線程來執(zhí)行所有這些Finalizer,而GC線程繼續(xù)去刪除其他待回收的對象。在下一個GC周期,這些執(zhí)行完Finalizer的對象的內(nèi)存才會被回收。
.NET GC使用"代"(generations)的概念來優(yōu)化性能。代幫助GC更迅速的識別那些最可能成為垃圾的對象。在上次執(zhí)行完垃圾回收后新創(chuàng)建的對象為第0代對象。經(jīng)歷了一次GC周期的對象為第1代對象。經(jīng)歷了兩次或更多的GC周期的對象為第2代對象。代的作用是為了區(qū)分局部變量和需要在應(yīng)用程序生存周期中一直存活的對象。大部分第0代對象是局部變量。成員變量和全局變量很快變成第1代對象并最終成為第2代對象。
GC對不同代的對象執(zhí)行不同的檢查策略以優(yōu)化性能。每個GC周期都會檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。重新思考Finalization的代價:需要Finalization的對象可能比不需要Finalization在內(nèi)存中停留額外9個GC周期。如果此時它還沒有被Finalize,就變成第2代對象,從而在內(nèi)存中停留更長時間。
為什么要使用GC呢?
提高了軟件開發(fā)的抽象度;
程序員可以將精力集中在實際的問題上而不用分心來管理內(nèi)存的問題;
可以使模塊的接口更加的清晰,減小模塊間的偶合;
大大減少了內(nèi)存人為管理不當(dāng)所帶來的Bug;
使內(nèi)存管理更加高效。
總的說來GC可以使程序員可以從復(fù)雜的內(nèi)存問題中擺脫出來,從而提高了軟件開發(fā)的速度、質(zhì)量和安全性。


有人靠"搶茅臺"月入百萬,腳本曝光,開源可用!

【古馳×張若昀×平安人壽】這3款紅包封面強勢來襲,趕快領(lǐng)取,手慢者無!
