由於垃圾收集演算法的實現涉及大量的程式細節,而且每個平臺的虛擬機器操作記憶體的方法又各不相同,因此部落格中不過多的討論演算法的實現,只是介紹幾種演算法的思想以及發展。
相關閱讀:
1、標記-清除演算法
標記清除演算法分為“標記”和“清除”兩個階段,首先先標記出那些物件需要被回收,在標記完成後會對這些被標記了的物件進行回收;如下圖:
這種演算法的優點在於不需要對物件進行移動操作,僅對不存活的物件進行操作,所以在物件存活率較高的情況下效率非常高,但是從上圖模擬的結果來看物件被回收後,可用的記憶體並不是連續的,而是斷斷續續,造成大量的記憶體碎片。 儲存物件時要求記憶體空間時連續的,所以虛擬機器在給新的記憶體較大的物件分配空間時,有可能找不到足夠大的連續的空閒的空間來存放,從而引發一次垃圾回收動作,實際上裡面是有大量的空閒空間的,只是不連續而已。
2、複製演算法
複製演算法是將記憶體分為兩塊大小一樣的區域,每次是使用其中的一塊。當這塊記憶體塊用完了,就將這塊記憶體中還存活的物件複製到另一塊記憶體中,然後清空這塊記憶體。這種演算法在物件存活率較低的場景下效率很高,比如說新生代,只對整塊記憶體區域的一半進行垃圾回收,在垃圾回收的過程也不會出現記憶體碎片的情況,不需要移動物件,只需要移動指標即可,實現簡單,所以執行效率很高。執行效率是在建立在浪費空間的基礎上的,這是典型的已空間換時間的方法,因為每次只能是使用北村的一半。演算法示意圖如下:
現在商用的jvm中都採用了這種演算法來回收新生代,因為新生代的物件基本上都是朝生夕死的,存活下來的物件約佔10%左右,所以需要複製的物件比較少,採用這種演算法效率比較高。hotspot版本的虛擬機器將堆(heap)記憶體分為了新生代和老年代,其中新生代又分為記憶體較大的Eden區和兩個較小的survivor區。當進行記憶體回收時,將eden區和survivor區的還存活的物件一次性地複製到另一個survivor空間上,最後將eden區和剛才使用過的survivor空間清理掉。hotspot虛擬機器預設eden和survivor空間的大小比例為8:1,也就是每次新生代中可用記憶體空間為整個新生代空間的90%(80%+10%),只會浪費掉10%的空間。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當survivor空間不夠用時,需要依賴於其他記憶體(這裡指的是老年代)進行分配的擔保。
3、標記-整理演算法
複製演算法在物件存活率較高的情況下就要進行較多的物件複製操作,效率將會變低。更關鍵的是,如果你不需要浪費50%的空間,就需要有額外的空間進行分配擔保,用以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種辦法。
根據老年代的特點,有人提出了標記-整理的演算法,標記過程仍然與標記-清楚演算法一樣,但後續步驟不是直接將可回收物件清理掉,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,演算法示意圖如下:
4、分代收集演算法
分代收集演算法將heap區域劃分為新生代和老年代,新生代的空間比老年代的空間要小。新生代又分為了Eden和兩個survivor空間,它們的比例為8:1:1。物件被建立時,記憶體的分配是在新生代的Eden區發生的,大物件直接在老年代分配記憶體,IBM的研究表明,Eden區98%的物件都是很快消亡的。
為了提高gc效率,分代收集演算法中新生代和老年代的gc是分開的,新生代發生的gc動作叫做minor gc 或 young gc,老年代發生的叫做major gc 或 full gc。
minor gc 的觸發條件:當建立新物件時Eden區剩餘空間小於物件的記憶體大小時發生minor gc;
major gc 觸發條件:
1、顯式呼叫System.gc()方法;
2、老年代空間不足;
3、方法區空間不足;
4、從新生代進入老年代的空間大於老年代空閒空間;
Eden區物件的特點是生命週期短,存活率低,因此Eden區使用了複製演算法來回收物件,上面也提到複製演算法的特點是在存活率較低的情況下效率會高很多,因為需要複製的物件少。與一般的複製演算法不同的是,一般的複製演算法每次只能使用一半的空間,另一半則浪費掉了,Eden區的回收演算法也叫做"停止-複製"演算法,當Eden區空間已滿時,觸發Minor GC,清理掉無用的物件,然後將存活的物件複製到survivor1區(此時survivor0有存活物件,survivor1為空的),清理完成後survivor0為空白空間,survivor1有存活物件,然後將survivor0和survivor1空間的角色物件,下次觸發Minor gc時重複上述過程。如果survivor1區剩餘空間小於複製物件所需空間時,將物件分配到老年代中。每發生一次Minor gc時,存活下來的物件的年齡則會加1,達到一定的年齡後(預設為15)該物件就會進入到老年代中。
老年代的物件基本是經過多次Minor gc後存活下來的,因此他們都是比較穩定的,存活率高,如果還是用複製演算法顯然是行不通的。所以老年代使用“標記-整理”演算法來回收物件的,從而提高老年代回收效率。
總的來說,分代收集演算法並不是一種具體的演算法,而是根據每個年齡代的特點,多種演算法結合使用來提高垃圾回收效率。
參考資料:《深入理解Java虛擬機器》