GC(Garbage Collector)就是垃圾收集器,這里僅就內存而言。以應用程序的root為基礎,遍歷應用程序在Heap上動(dòng)態(tài)分配的所有對象,通過(guò)識別它們是否被引用來(lái)確定哪些對象是已經(jīng)死亡的、哪些仍需要被使用。已經(jīng)不再被應用程序的root或者別的對象所引用的對象就是已經(jīng)死亡的對象,即所謂的垃圾,需要被回收。這就是GC工作的原理。
為了實(shí)現這個(gè)原理,GC有多種算法。比較常見(jiàn)的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統.NET CLR,Java VM都是采用的Mark Sweep算法。
垃圾收集器的本質(zhì),就是跟蹤所有被引用到的對象,整理不再被引用的對象,回收相應的內存。
這聽(tīng)起來(lái)類(lèi)似于一種叫做“引用計數(Reference Counting)”的算法,然而這種算法需要遍歷所有對象,并維護它們的引用情況,所以效率較低些,并且在出現“環(huán)引用”時(shí)很容易造成內存泄露。
所以.Net中采用了一種叫做“標記與清除(Mark Sweep)”算法來(lái)完成上述任務(wù)。
“標記與清除”算法,顧名思義,這種算法有兩個(gè)本領(lǐng):
“標記”本領(lǐng)——垃圾的識別:從應用程序的root出發(fā),利用相互引用關(guān)系,遍歷其在Heap上動(dòng)態(tài)分配的所有對象,沒(méi)有被引用的對象不被標記,即成為垃圾;存活的對象被標記,即維護成了一張“根-對象可達圖”。其實(shí),CLR會(huì )把對象關(guān)系看做“樹(shù)圖”,這樣會(huì )加快遍歷對象的速度。
.Net中利用棧來(lái)完成檢測并標記對象引用,在不斷的入棧與出棧中完成檢測:先在樹(shù)圖中選擇一個(gè)需要檢測的對象,將該對象的所有引用壓棧,如此反復直到棧變空為止。棧變空意味著(zhù)已經(jīng)遍歷了這個(gè)局部根能夠到達的所有對象。樹(shù)圖節點(diǎn)范圍包括局部變量、寄存器、靜態(tài)變量,這些元素都要重復這個(gè)操作。一旦完成,便逐個(gè)對象地檢查內存,沒(méi)有標記的對象變成了垃圾。
“清除”本領(lǐng)——回收內存:?jiǎn)⒂脡嚎s(Compact)算法,對內存中存活的對象進(jìn)行移動(dòng),修改它們的指針,使之在內存中連續,這樣空閑的內存也就連續了,這就解決了內存碎片問(wèn)題,當再次為新對象分配內存時(shí),CLR不必在充滿(mǎn)碎片的內存中尋找適合新對象的內存空間,所以分配速度會(huì )大大提高。但是大對象(large object heap)除外,GC不會(huì )移動(dòng)一個(gè)內存中巨無(wú)霸,因為它知道現在的CPU不便宜。通常,大對象具有很長(cháng)的生存期,當一個(gè)大對象在.NET托管堆中產(chǎn)生時(shí),它被分配在堆的一個(gè)特殊部分中,移動(dòng)大對象所帶來(lái)的開(kāi)銷(xiāo)超過(guò)了整理這部分堆所能提高的性能。
Compact算法除了會(huì )提高再次分配內存的速度,如果新分配的對象在堆中位置很緊湊的話(huà),高速緩存的性能將會(huì )得到提高,因為一起分配的對象經(jīng)常被一起使用(程序的局部性原理),所以為程序提供一段連續空白的內存空間是很重要的。
簡(jiǎn)單地把.NET的GC算法看作Mark-Sweep 算法。
階段1: Mark-Sweep 標記清除階段,先假設heap中所有對象都可以回收,然后找出不能回收的對象,給這些對象打上標記,最后heap中沒(méi)有打標記的對象都是可以被回收的;
階段2: Compact 壓縮階段,對象回收之后heap內存空間變得不連續,在heap中移動(dòng)這些對象,使他們重新從heap基地址開(kāi)始連續排列,類(lèi)似于磁盤(pán)空間的碎片整理。
Mark-Sweep 算法.png
Reachable objects:指根據對象引用關(guān)系,從roots出發(fā)可以到達的對象。例如當前執行函數的局部變量對象A是一個(gè)root object,他的成員變量引用了對象B,則B是一個(gè)reachable object。從roots出發(fā)可以創(chuàng )建reachable objects graph,剩余對象即為unreachable,可以被回收 。
.NET將heap分成3個(gè)代齡區域: Gen 0、Gen 1、Gen 2;heap分配的對象是連續的,關(guān)聯(lián)度較強有利于提高CPU cache的命中率。
Generational 分代算法.png
Heap分為3個(gè)代齡區域,相應的GC有3種方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。
如果Gen 0 heap內存達到閥值,則觸發(fā)0代GC,0代GC后Gen 0中幸存的對象進(jìn)入Gen1。如果Gen 1的內存達到閥值,則進(jìn)行1代GC,1代GC將Gen 0 heap和Gen 1 heap一起進(jìn)行回收,幸存的對象進(jìn)入Gen2。2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收。如果GC跑過(guò)了,內存空間依然不夠用,那么就拋出了OutOfMemoryException異常。
Gen 0和Gen 1比較小,這兩個(gè)代齡加起來(lái)總是保持在16M左右;Gen2的大小由應用程序確定,可能達到幾G,因此0代和1代GC的成本非常低,2代GC稱(chēng)為full GC,通常成本很高。
粗略的計算0代和1代GC應當能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時(shí),full GC可能需要花費幾秒時(shí)間。大致上來(lái)講.NET應用運行期間,2代、1代和0代GC的頻率應當大致為1:10:100。
既然有了垃圾收集器,為什么還要Dispose方法和析構函數?
因為CLR的緣故,GC只能釋放托管資源,不能釋放非托管資源(數據庫鏈接、文件流等)。所以對于非托管資源一般我們會(huì )選擇為類(lèi)實(shí)現IDispose接口,寫(xiě)一個(gè)Dispose方法。讓調用者手動(dòng)調用這個(gè)類(lèi)的Dispose方法(或者用using語(yǔ)句塊來(lái)自動(dòng)調用Dispose方法),Dispose執行時(shí),析構函數和垃圾收集器都還沒(méi)有開(kāi)始處理這個(gè)對象的釋放工作。
如果我們不想為一個(gè)類(lèi)實(shí)現Dispose方法,而是想讓它自動(dòng)的釋放非托管資源,那么就要用到析構函數了。析構函數是由GC調用的。你無(wú)法預測析構函數何時(shí)會(huì )被調用,所以盡量不要在這里操作可能被回收的托管資源,析構函數只用來(lái)釋放非托管資源。GC釋放包含析構函數的對象,需要垃圾處理器調用倆次,CLR會(huì )先讓析構函數執行,再收集它占用的內存。
關(guān)于如何釋放非托管資源詳情,可以看一下另一篇文章《C#之托管與非托管資源》
GC什么時(shí)候執行垃圾收集是一個(gè)非常復雜的算法(策略),大概可以描述成這樣:如果GC發(fā)現上一次收集了很多對象,釋放了很大的內存,那么它就會(huì )盡快執行第二次回收,如果它頻繁的回收,但釋放的內存不多,那么它就會(huì )減慢回收的頻率。所以,盡量不要調用GC.Collect()這樣會(huì )破壞GC現有的執行策略。除非你對你的應用程序內存使用情況非常了解,你知道何時(shí)會(huì )產(chǎn)生大量的垃圾,那么你可以手動(dòng)干預垃圾收集器的工作,例如我有一個(gè)大對象,我擔心GC要過(guò)很久才會(huì )收集他。
作用:強制進(jìn)行垃圾回收。
| 名稱(chēng) | 說(shuō)明 |
|---|---|
| Collect() | 強制對所有代進(jìn)行即時(shí)垃圾回收。 |
| Collect(Int32) | 強制對零代到指定代進(jìn)行即時(shí)垃圾回收 |
| Collect(Int32, GCCollectionMode) | 強制在 GCCollectionMode 值所指定的時(shí)間對零代到指定代進(jìn)行垃圾回收 |
只管理內存,非托管資源,如文件句柄,GDI資源,數據庫連接等還需要用戶(hù)去管理。
循環(huán)引用,網(wǎng)狀結構等的實(shí)現會(huì )變得簡(jiǎn)單。GC的標志-壓縮算法能有效的檢測這些關(guān)系,并將不再被引用的網(wǎng)狀結構整體刪除。
GC通過(guò)從程序的根對象開(kāi)始遍歷來(lái)檢測一個(gè)對象是否可被其他對象訪(fǎng)問(wèn),而不是用類(lèi)似于COM中的引用計數方法。
GC在一個(gè)獨立的線(xiàn)程中運行來(lái)刪除不再被引用的內存。
GC每次運行時(shí)會(huì )壓縮托管堆。
你必須對非托管資源的釋放負責??梢酝ㄟ^(guò)在類(lèi)型中定義Finalizer來(lái)保證資源得到釋放。
對象的Finalizer被執行的時(shí)間是在對象不再被引用后的某個(gè)不確定的時(shí)間。注意并非和C++中一樣在對象超出聲明周期時(shí)立即執行析構函數
Finalizer的使用有性能上的代價(jià)。需要Finalization的對象不會(huì )立即被清除,而需要先執行Finalizer.Finalizer,不是在GC執行的線(xiàn)程被調用。GC把每一個(gè)需要執行Finalizer的對象放到一個(gè)隊列中去,然后啟動(dòng)另一個(gè)線(xiàn)程來(lái)執行所有這些Finalizer,而GC線(xiàn)程繼續去刪除其他待回收的對象。在下一個(gè)GC周期,這些執行完Finalizer的對象的內存才會(huì )被回收。
.NET GC使用"代"(generations)的概念來(lái)優(yōu)化性能。代幫助GC更迅速的識別那些最可能成為垃圾的對象。在上次執行完垃圾回收后新創(chuàng )建的對象為第0代對象。經(jīng)歷了一次GC周期的對象為第1代對象。經(jīng)歷了兩次或更多的GC周期的對象為第2代對象。代的作用是為了區分局部變量和需要在應用程序生存周期中一直存活的對象。大部分第0代對象是局部變量。成員變量和全局變量很快變成第1代對象并最終成為第2代對象。
GC對不同代的對象執行不同的檢查策略以?xún)?yōu)化性能。每個(gè)GC周期都會(huì )檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。重新思考Finalization的代價(jià):需要Finalization的對象可能比不需要Finalization在內存中停留額外9個(gè)GC周期。如果此時(shí)它還沒(méi)有被Finalize,就變成第2代對象,從而在內存中停留更長(cháng)時(shí)間。
提高了軟件開(kāi)發(fā)的抽象度;
程序員可以將精力集中在實(shí)際的問(wèn)題上而不用分心來(lái)管理內存的問(wèn)題;
可以使模塊的接口更加的清晰,減小模塊間的偶合;
大大減少了內存人為管理不當所帶來(lái)的Bug;
使內存管理更加高效。
總的說(shuō)來(lái)GC可以使程序員可以從復雜的內存問(wèn)題中擺脫出來(lái),從而提高了軟件開(kāi)發(fā)的速度、質(zhì)量和安全性。
聯(lián)系客服