深入理解java虛擬機-垃圾收集算法

從如何判定對象消亡的角度出發,垃圾收集算法可以劃分爲“引用計數式垃圾收集”和“追蹤式垃圾收集”兩大類,這兩類也被稱爲“直接垃圾收集”和“間接垃圾收集”。

1.分代收集理論

兩個分代假說:

(1)弱分代假說:絕大多數對象都是朝生夕滅的

(2)強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡

奠定了垃圾收集器的一致設計原則:

收集器應該將java堆劃分出不同的區域,然後將回收對象依據其年齡分配到不同的區域之中存儲。

把java堆劃分爲新生代,老年代兩個區域。在新生代中每次垃圾收集時都發現有大批對象死去,而每次回收後存活的少量對象,將會逐步晉升到老年代中存放。

分代收集並非只是簡單劃分一下內存區域那麼容易,它至少存在一個明顯的困難:對象不是孤立的,對象之間會存在跨代引用。假如要現在進行一次只侷限於新生代區域內的收集,但新生代中的對象完全有可能被老年代所引用,爲了找出該區域的存活對象,不得不在固定的GCRoots之外,再額外遍歷整個老年代中所有對象來確保可達性分析結果的正確性,反過來也是一樣。爲了解決這個問題,就需要對分代收集理論添加第三條經驗法則:

(3)跨代引用假說:跨代引用相對於同代引用來說僅佔極少數。

即存在相互引用關係的兩個對象是應該傾向於同時生存或者同時消亡的。比如:如果某個新生代對象存在跨代引用,由於老年代對象難以消亡,該引用會使得新生代對象在收集時同樣得以存活,進而隨着年齡增長之後晉升到老年代中,這是跨代引用也隨機被消除了。

依據這條假說,我們只需在新生代上建立一個全局的數據結構,把老年代劃分爲若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後當發生新生代區域收集時,只有包含了跨代引用的小塊內存裏的對象纔會被加入到GC Roots進行掃描。

2.標記-清除算法

算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,回收所有未被標記的對象。標記過程就是對象是否處於垃圾的判定過程。

標記-清除算法缺點:

1.執行效率不穩定,如果java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除動作,導致標記和清除兩個過程的執行效率都隨着對象數量增長而降低;

2.內存空間碎片化問題,標記,清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

3.標記-複製算法

爲了解決標記-清除算法面對大量可回收對象時執行效率低的問題,Fenichel提出了一種稱爲半區複製垃圾收集算法,它將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次性清理掉。如果內存中多數對象都是存活的,這種算法將會產生大量的內存空間開銷,但對於多數對象都是可回收的情況,算法需要複製的就是佔少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時就不用了考慮空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。

缺點:

這種複製回收算法的代價是將可用內存縮小爲原來的一半,空間浪費太多。

Andrew Appel針對具備“朝生夕滅”特點的對象,提出了一種更優化的半區複製分代策略,現在稱爲“Appel式回收”。

具體做法:把新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配只使用Eden和其中一塊Survivor。發生垃圾收集時,將Eden和Survivor中仍然存活的對象一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已經用過的那塊Survivor空間。Eden和Survivor的大小比例是8:1,每次新生代中可用內存空間爲整個新生代容量的90%,只有一個Survivor空間是被“浪費的”。任何人都沒有辦法保證每次回收只有不多於10%的對象存活,當Survivor空間不足以容納一次MinorGC之後的存活的對象時,就需要依賴其他內存區域進行分配擔保。如果另一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象便將通過分配擔保機制直接進入老年代,這對虛擬機來說就是安全的。

4.標記-整理算法

標記-複製算法在對象存活率較高時就要進行較多的複製操作,效率將會降低。如果不想浪費50%的空間,就需要額外的空間進行復制擔保,以應對內存中所有對象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。

標記過程仍然與“標記-清除”算法一樣,讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。標記清除算法與標記整理算法的本質差異在於前者是一種非移動式的回收算法,而後者是移動式的。

如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極爲負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行。

但如果跟標記清除算法那樣完全不考慮移動和整理存活對象的話,彌散於堆中的存活對象導致的空間碎片化問題就只能依賴於更爲複雜的內存分配器和內存訪問器來解決。譬如通過“分區空閒分配鏈表”來解決內存分配問題。

基於以上兩點,是否移動對象都存在弊端,移動則內存回收時會更復雜,不移動則內存分配時會更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間更短,甚至不需要停頓,但從整個程序的吞吐量來看,移動對象會更划算。

還有一種做法“和稀泥式”解決方案可以不在內存分配和訪問上增加太大額外負擔。做法是讓虛擬機平時大多數時間都採用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經達到影響對象分配時,再採用標記-整理算法收集一次,以獲得規整的內存空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章