垃圾回收算法看這一篇就夠了

簡介

本文介紹了常見的三種垃圾回收算法(mark-sweep,mark-compact,mark-copy),是java虛擬機各類垃圾收集器的算法基礎,爲以後學習hotspot虛擬機的垃圾收集器打下基礎java

垃圾收集器解決的問題

1 開發者可能過早的回收依然在引用的對象,這種狀況將引起懸掛指針問題。算法

2 開發者可能在程序將對象使用完畢以後未將對象釋放,從而致使內存泄漏;緩存

垃圾收集的好處

1 不會有二次釋放的問題;安全

2 下降了程序的耦合度,開發者只需關注自身模塊,或只關注其餘相關模塊的少許代碼,顯示的內存管理沒法知足軟件工程的低耦合的原則,須要引入一些額外的接口。數據結構

3 完整性與及時性併發

理想狀況下,垃圾收集過程應當是完整的,即堆中的全部垃圾都應當獲得回收,但這一般是不現實的,從性能方面考慮,再一次回收過程當中只處理堆中部分對象或許更加合理,例如分代回收器會依照堆中的年齡將其劃分爲兩代或者更多代,並把經歷集中在年輕代,這樣不只提升了回收效率,也能夠減小單次回收的停頓時間;在併發垃圾回收器中,賦值器與回收器同時工做,其目的在於避免或者儘可能減小用戶程序的停頓,此類回收器會遇到浮動垃圾的問題,即對象在回收過程啓動後才變成垃圾,那麼該對象只能在下一個回收週期內獲得回收,所以在併發回收器中,衡量完整性更好的方法是統計全部垃圾的最終回收狀況,而不是單個回收週期的回收狀況。佈局

4停頓時間性能

許多回收器在進行垃圾回收時須要終端賦值器線程,所以會致使在線程執行過程當中停頓,回收器應當儘可能減小對程序的執行過程的影響,所以停頓時間越短越好,例如分代式回收器經過頻繁且快速的回收較小的,較爲年輕的對象來縮短停頓時間,而較大的,較爲年老的對象回收則只是偶爾進行(真是個喜歡年輕對象的渣男)。學習

5 空間開銷優化

內存管理的目的是安全且高效的使用內存空間,不管是顯示的內存管理仍是自動的內存管理,不一樣的管理策略均會產生不一樣程度的空間開銷,某些垃圾收集器須要在每一個對象內部佔用必定的空間(例如保存引用計數),還有些收集器會服用 對象現有的佈局上已經存在的域(轉發指針記錄在用戶數據上)。回收器也可能引入堆級別的內存開銷,好比copying式回收器須要將對分爲兩個半區,任什麼時候候賦值器只能使用一個半區,另外一個半區會被回收器所保留,並在回收過程當中將存活對象複製到其中;回收器有時也須要一些輔助的數據結構,好比追蹤式的回收器要經過標記棧來引導堆中指針圖表的遍歷,也就是一般所說的根節點枚舉,根據gc root set遍歷全堆,回收器標記對象時也可使用額外的位圖(bitmap),對於一些須要將堆分爲數個獨立區域的回收器,須要額外的記憶集來保存賦值器所修改的指針和跨區域的指針的位置。

4種垃圾收集策略

標記-清掃(mark-sweep)、標記-複製(mark-copy)、標記-整理(mark-compact),引用計數時4種最基本的垃圾回收策略,大多數回收器會以不一樣的方式對這些基本回收策略進行組合

1 標記-清掃(sweep)

標記-清掃算法與賦值器的接口十分簡單:若是線程沒法分配對象,則喚起收集器,而後再次嘗試分配

回收器在遍歷對象圖以前必須先構造標記過程須要用到的起始工做列表(gc root set),即對每一個根對象進行標記並將其加入工做列表,回收器能夠經過標記對象頭的某個位(或者字節)的方式對其進行標記,該位(字節)也可位於一張額外的表中

須要注意的是:標記-清掃回收器要求堆佈局知足必定的條件:

  • mark-sweep回收器不會移動對象,所以內存管理器必須可以控制堆內存碎片,由於過多的內存碎片可能會致使分配器沒法知足新分配的請求,從而增長垃圾回收的頻率
  • 清掃器必須可以遍歷堆中每個對象;

對sweep回收器的優化

1 位圖標記

回收器能夠將對象標記位保存在器頭部的某個字中,也可使用一個獨立的位圖來維護標記位,位圖能夠有一個,也能夠存在多個,好比在塊結構的堆中,回收器能夠爲每一個內存塊維護一個獨立的位圖,這一方式能夠避免因爲堆不連續致使的內存浪費。

使用位圖標記能夠減小回收過程當中的換頁次數,任何由回收器致使的換頁行爲一般都是不可接受的,對象每每成簇誕生併成批死亡,而許多分配器也會吧這些對象分配在相鄰的空間。使用位圖來引導清掃能夠批量讀取/清空一批對象的標記位,並且經過位圖標記能夠簡單的判斷某一內存塊中的全部對象是否都是垃圾,進而能夠一次性回收整個內存塊

對於使用標記位放置在對象頭部這一策略,位圖可使得標記位更加密集;對於使用sweep的回收器,標記過程只需讀取存活對象的指針域而不會修改任何對象;清掃器不會對存活對象進行任何的讀寫操做,只會在釋放垃圾對象的過程當中覆蓋某些域,所以位圖能夠減小內存中須要修改的字節數,並且減小了對高速緩存的寫入

2 懶惰清掃

標記過程的時間複雜度是O(L),其中L是堆中存活對象的數量,清掃過程的時間複雜度是O(H),H爲堆大小,雖然H>L,但mark過程的內存訪問時不可預測的,而清掃過程的可預測性就要高的多,並且清掃對象的開銷也比追蹤對象的開銷小的多,mark-sweep算法中一般會將大小相同的對象分佈在連續的空間內,此時回收器能夠依照固定的步幅對大小相同的對象進行清掃。

Lazy sweeping 該方案利用分配器來扮演清掃器的角色,即把尋找可用空間的任務轉移到allocate過程當中,從而不須要單獨的清掃階段,最簡單的清掃策略是,allocate簡單的向前移動清掃指針,直到在連續的未標記對象中找到一塊足夠大的空間

分配器一般只會在一個內存塊中分配相同大小的對象,每一個空間大小分級都會對應一個或多個用於分配的內存塊,以及一個待回收內存塊鏈表,在回收過程當中,回收器依然須要將堆中全部存活對象標記,但標記或回收器不急於清掃整個堆,而是簡單的將徹底爲空的內存塊歸還給塊分配器,同時將其餘內存塊添加到其所對應空間大小分級的隊列中,一旦stop the world結束,賦值器馬上開始工做,對於任意內存的分配需求,allocate方法首先嚐試從合適的空間大小分級中分配一個空閒槽,若是失敗則調用清掃器執行懶惰清掃,即從該空間大小分級的回收隊列中取出一個或多個內存塊進行清掃,知道知足分配要求爲止,若是沒有內存塊可供清掃,或者清掃的內存塊不包含熱河空閒槽,分配器便嘗試從更低級別的塊分配器中獲取新內存塊

mark-sweep算法須要考慮的問題

1 吞吐量

mark-sweep式回收器在執行過程當中須要颳起全部賦值器線程,回收停頓時間取決於應用程序的運行及其輸入

2 空間利用率

與mark-copy相比,mark-sweep具備更高的空間利用率,但sweep回收會產生一些空間碎片,cms的作法是能夠隔一段時間採用compact算法整理一次空間碎片

3 是否移動對象

sweep最大的優點就是不會移動對象,

分代回收中,年老代被GC時對象存活率可能會很高,並且假定可用剩餘空間不太多,這樣copying算法就不太合適,因而更可能選用另兩種算法,特別是不用移動對象的mark-sweep算法。

不過HotSpot VM中除了CMS以外的其它收集器都是會移動對象的,也就是要麼是copying、要麼是mark-compact的變種。

標記-整理(mark-compact)

爲了解決sweep中的內存碎片問題,須要對堆中存活對象進行整理以下降內存碎片的回收策略,compact能夠極爲快速的順序分配

Mark-compact算法執行要通過數個階段:首先是標記階段,而後是整理階段,即移動存活對象,同時更新存活對象中執行被移動對象對象的指針,有一線三種compact順序

  • 任意順序:對象的移動方式與它們的原始排列順序和飲用關係無關
  • 線性順序:將具備關聯關係的對象排列在一塊兒,如具備引用關係的對象,或者統一數據結構中的相鄰對象
  • 滑動順序: 將對象滑動到堆的一頓啊,擠出垃圾,從而保證對象在堆中的原有分配順序

咱們所瞭解的整理式回收器大多遵循任意順序和滑動順序

任意順序實現簡單,且執行速度快,特別是全部對象大小相等的狀況,但任意順序整理可能會將原來相鄰的對象分散到不一樣的高速緩存行或者虛擬內存頁中,從而下降賦值器空間局部性。因此現代compact回收器均使用滑動整理順序

下面介紹幾種不一樣類型的整理算法

1 雙指針整理算法

起始階段,指針free指向區域始端,指針scan指向區域末端,在第一次遍歷過程當中,回收器不斷向後移動指針free,直到在堆中發現空隙爲止;指針scan從後往前移動,知道發現存活對象,若是兩個指針交錯,則該階段結束,不然便將指針scan所指向的對象移動到指針free的位置,同時將原對象中的某個域修改成轉發地址,而後繼續移動指針。雙指針的有點事簡單快速,且遍歷的過程操做較少,但肯定是雙指針重排序對象的順序是任意式的,所以破壞了賦值器的局部性,而後因爲相關對象老是成簇誕生,成批死亡,咱們能夠將連續存活對象總體移動到較大空隙中,而不是逐個移動

lisp2算法

該算法要通過三次遍歷,但每次遍歷作的工做很少;

在標記結束的第一次遍歷中,回收器計算出每一個存活對象的最終地址,並保存在對象的forwardingAddres域中,

第二次遍歷過程當中,回收器將使用對象頭域中記錄轉發地址來更新賦值器線程根以及被標記對象的引用,該操做將確保他們指向對象的新位置

第三次遍歷過程當中,relocate最終將每一個存活對象移動到新的目標位置

須要考慮的問題

吞吐量

與標記mark-sweep相比,mark-compact算法須要更屢次遍歷,所以吞吐量較差,每次遍歷的開銷都很大,一個通用的解決方案是,儘可能長久的使用mark-sweep算法,在碎片化達到必定程度後,才使用mark-compact算法回收一次

長壽數據

Mark-compact算法能夠選擇不去整理「沉積區」內的對象,付出的代價是存在少許的內存碎片

局部性

採用不一樣的compact算法會獲得不一樣的局部性,在雖然隨機算法簡單高效,但會破壞賦值器的局部性,因此滑動式的整理算法對局部性更友好

標記-複製(mark-copy)

sweep回收的開銷較低,但其存在內存碎片的問題,在一個良好的系統中,垃圾回收一般只佔總體執行時間的一小部分,賦值器的執行開銷將決定整個程序的性能,所以應設法下降賦值器的開銷,特別是儘可能提高它的分配速度,compact能夠消除內存碎片,但須要屢次遍歷,copy算法回收器在複製過程當中進行堆整理,從而提高了賦值器的分配速度,可是堆的可用空間下降了一半

須要考慮的問題

分配

通過整理的堆分配內存速度很快,分配過程簡單;

空間與局部性

copy算法的肯定就是須要維護第二個半區,在內存大小必定,半區複製算法的可用空間是整堆回收的通常,這致使複製式的回收器所需的回收次數要比其餘回收器更多;

而局部性首遍歷方式所影響廣度優先遍歷順序有將父子節點分開的趨勢,而深度優先遍歷則趨向於子節點預期賦節點排列的更近

移動對象

是否使用複製式取決於移動對象的開銷,在分代式的回收器中,老年代存活數量多,而且有大對象,不適合使用copy算法,單年輕代存活數量少,且對象比較小,適合使用mark-copy

三種算法對比

分代式GC裏,年老代經常使用mark-sweep;或者是mark-sweep/mark-compact的混合方式,通常狀況下用mark-sweep,統計估算碎片量達到必定程度時用mark-compact。這是由於傳統上你們認爲年老代的對象可能會長時間存活且存活率高,或者是比較大,這樣拷貝起來不划算,還不如採用就地收集的方式。Mark-sweep、mark-compact、copying這三種基本算法裏,只有mark-sweep是不移動對象(也就是不用拷貝)的,因此選用mark-sweep(cms)。

簡要對比三種基本算法:

mark-sweep mark-compact copying
速度 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 一般須要活對象的2倍大小(不堆積碎片)
移動對象?

關於時間開銷: mark-sweep:mark階段與活對象的數量成正比,sweep階段與整堆大小成正比 mark-compact:mark階段與活對象的數量成正比,compact階段與活對象的大小成正比 copying:與活對象大小成正比

若是把mark、sweep、compact、copying這幾種動做的耗時放在一塊兒看,大體有這樣的關係: compaction >= copying > marking > sweeping 還有 marking + sweeping > copying (雖然compactiont與copying都涉及移動對象,但取決於具體算法,compact可能要先計算一次對象的目標地址,而後修正指針,而後再移動對象;copying則能夠把這幾件事情合爲一體來作,因此能夠快一些。 另外還須要留意GC帶來的開銷不能只看collector的耗時,還得看allocator一側的。若是能保證內存沒碎片,分配就能夠用pointer bumping方式,只有挪一個指針就完成了分配,很是快;而若是內存有碎片就得用freelist之類的方式管理,分配速度一般會慢一些。)

在分代式假設中,年輕代中的對象在minor GC時的存活率應該很低,這樣用copying算法就是最合算的,由於其時間開銷與活對象的大小成正比,若是沒多少活對象,它就很是快;並且young gen自己應該比較小,就算須要2倍空間也只會浪費不太多的空間。 而年老代被GC時對象存活率可能會很高,並且假定可用剩餘空間不太多,這樣copying算法就不太合適,因而更可能選用另兩種算法,特別是不用移動對象的mark-sweep算法。

不過HotSpot VM中除了CMS以外的其它收集器都是會移動對象的,也就是要麼是copying、要麼是mark-compact的變種。

這一篇寫了很久,參考了一些垃圾算法的數據和文獻,下一篇想寫hotspot的垃圾收集的細節,和hotspot中的垃圾數據器的優缺點以及查看gc日誌,優化垃圾收集時代碼中的注意點。

相關文章
相關標籤/搜索