Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法

Mr羽墨青衫發表於2019-01-18

GC的出現解放了程式設計師需要手動回收記憶體的苦惱,但我們也是要了解GC的,知己知彼,百戰不殆嘛。 常見的GC回收演算法主要包括引用計數演算法、可達性分析法、標記清除演算法、複製演算法、標記壓縮演算法、分代演算法以及分割槽演算法。

其中,引用計數法和可達性分析法用於判定一個物件是否可以回收,其他的演算法為具體執行GC時的演算法。

今天來聊聊標記清除演算法、複製演算法、標記壓縮演算法、分代演算法,主要介紹分代演算法。

引用計數法和可達性分析法請移步:

引用計數法

可達性分析法

1標記清除演算法

標記清除法是現在GC演算法的基礎,目前似乎沒有哪個GC還在使用這種演算法了。因為這種演算法會產生大量的記憶體碎片。

標記清除演算法的執行過程分為兩個階段:標記階段、清除階段。

  • 標記階段會通過可達性分析將不可達的物件標記出來。
  • 清除階段會將標記階段標記的垃圾物件清除。

標記階段如圖所示:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
Java堆中,黃色物件為不可達物件,在標記階段被標記。

下面執行回收演算法,執行後如圖:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
從上圖可以清晰的看出此演算法的缺陷,回收後會產生大量不連續的記憶體空間,即記憶體碎片。由於Java在分配記憶體時通常是按連續記憶體分配,那麼當碎片空間不足以分配給新的物件時,就造成了記憶體浪費。

2複製演算法

複製演算法會將記憶體空間分為兩塊,每次只使用其中一塊記憶體。複製演算法同樣使用可達性分析法標記除垃圾物件,當GC執行時,會將非垃圾物件複製到另一塊記憶體空間中,並且保證記憶體上的連續性,然後直接清空之前使用的記憶體空間。然後如此往復。

我們姑且將這兩塊記憶體區域稱為from區和to區。

如下圖所示,r1和r2作為GC Root物件,經過可達性分析後,標記除黃色物件為垃圾物件。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
複製過程如下,GC會將五個存活物件複製到to區,並且保證在to區記憶體空間上的連續性。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
最後,將from區中的垃圾物件清除。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
綜上述,該演算法在存貨物件少,垃圾物件多的情況下,非常高效。其好處是不會產生記憶體碎片,但壞處也是顯而易見的,就是直接損失了一半的可用記憶體。

3標記壓縮演算法

標記壓縮演算法可以解決標記清除演算法的記憶體碎片問題。 其演算法可以看作三步:

  • 標記垃圾物件
  • 清除垃圾物件
  • 記憶體碎片整理

其過程如下:

首先標記除垃圾物件(黃色)

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
清除垃圾物件

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
記憶體碎片整理

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法

4分代演算法

分代演算法基於複製演算法和標記壓縮演算法。

首先,標記清除演算法、複製演算法、標記壓縮演算法都有各自的缺點,如果單獨用其中某一演算法來做GC,會有很大的問題。

例如,標記清除演算法會產生大量的記憶體碎片,複製演算法會損失一半的記憶體,標記壓縮演算法的碎片整理會造成較大的消耗。

其次,複製演算法和標記壓縮演算法都有各自適合的使用場景。

複製演算法適用於每次回收時,存活物件少的場景,這樣就會減少複製量。

標記壓縮演算法適用於回收時,存活物件多的場景,這樣就會減少記憶體碎片的產生,碎片整理的代價就會小很多。

分代演算法將記憶體區域分為兩部分:新生代和老年代。

根據新生代和老年代中物件的不同特點,使用不同的GC演算法。

新生代物件的特點是:建立出來沒多久就可以被回收(例如虛擬機器棧中建立的物件,方法出棧就會銷燬)。也就是說,每次回收時,大部分是垃圾物件,所以新生代適用於複製演算法。

老年代的特點是:經過多次GC,依然存活。也就是說,每次GC時,大部分是存活物件,所以老年代適用於標記壓縮演算法。

新生代分為eden區、from區、to區,老年代是一整塊記憶體空間,如下所示:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
分代演算法執行過程

首先簡述一下新生代GC的整個過程(老年代GC會在下面介紹):新建立的物件總是在eden區中出生,當eden區滿時,會觸發Minor GC,此時會將eden區中的存活物件複製到from和to中一個沒有被使用的空間中,假設是to區(正在被使用的from區中的存活物件也會被複制到to區中)。

有幾種情況,物件會晉升到老年代:

  • 超大物件會直接進入到老年代(受虛擬機器引數-XX:PretenureSizeThreshold引數影響,預設值0,即不開啟,單位為Byte,例如:3145728=3M,那麼超過3M的物件,會直接晉升老年代)
  • 如果to區已滿,多出來的物件也會直接晉升老年代
  • 複製15次(15歲)後,依然存活的物件,也會進入老年代

此時eden區和from區都是垃圾物件,可以直接清除。

PS:為什麼複製15次(15歲)後,被判定為高齡物件,晉升到老年代呢?

因為每個物件的年齡是存在物件頭中的,物件頭用4bit儲存了這個年齡數,而4bit最大可以表示十進位制的15,所以是15歲。

下面從JVM啟動開始,描述GC的過程。

JVM剛啟動並初始化完成後,幾塊記憶體空間分配完畢,此時狀態如上圖所示。

(1)新建立的物件總是會出生在eden區

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
(2)當eden區滿的時候,會觸發一次Minor GC,此時會從from和to區中找一個沒有使用的空間,將eden區中還存活的物件複製過去(第一次from和to都是空的,使用from區),被複制的物件的年齡會+1,並清除eden區中的垃圾物件。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
(3)程式繼續執行,又在eden區產生了新的物件,併產生了一個超大物件,併產生了一個複製後to區放不下的物件

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
(4)當eden區再次被填滿時,會再一次觸發Minor GC,這次GC會將eden區和from區中存活的物件複製到to區,並且物件年齡+1,超大物件會直接晉升到老年代,to區放不下的物件也會直接晉升老年代。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法

(5)程式繼續執行,假設經過15次複製,某一物件依然存活,那麼他將直接進入老年代。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
(6)老年代 Full GC

在進行Minor GC之前,JVM還有一步操作,他會檢查新生代所有物件使用的總記憶體是否小於老年代最大剩餘連續記憶體,如果上述條件成立,那麼這次Minor GC一定是安全的,因為即使所有新生代物件都進入老年代,老年代也不會記憶體溢位。如果上述條件不成立,JVM會檢視引數HandlePromotionFailure[1]是否開啟(JDK1.6以後預設開啟),如果沒開啟,說明Minor GC後可能會存在老年代記憶體溢位的風險,會進行一次Full GC,如果開啟,JVM還會檢查歷次晉升老年代物件的平均大小是否小於老年代最大連續記憶體空間,如果成立,會嘗試直接進行Minor GC,如果不成立,老年代執行Full GC。

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
eden區和from區的存活物件會複製到to區,超大物件和to區容納不下的物件會直接晉升老年代。當eden區滿時,觸發Minor GC,此時判斷老年代剩餘連續記憶體已經小於新生代所有物件佔用記憶體總和,假設HandlePromotionFailure引數開啟,JVM還會繼續判斷老年代剩餘連續記憶體是否大於歷次晉升老年代物件的平均大小,如圖所示,目前老年代還剩2個空間,如果之前平均每次晉升三個物件到老年代,剩餘空間小於平均值,會觸發Full GC。

老年代回收-標記:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
老年代回收-清除:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
老年代回收-碎片整理:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法
Minor GC存在的問題

Minor GC的問題在於,新生代的物件可能被老年代引用,而這種情況可達性分析是分析不到的,但這種情況的新生代物件是不應該被回收的。

HotSpot虛擬機器提供了一個解決方案:卡表。

這種方法會將老年代記憶體平均分為很多的卡片,每個卡片都包含一部分物件,然後維護一個卡表,卡表是一個陣列,每個元素指向一個卡片,並標識出這個卡片中有沒有指向新生代的物件,如果有標識為1。這樣一來,Minor GC只需要掃描卡表中標識為1的卡片即可,大大提升了效率。

卡表如下圖所示:

Java虛擬機器-GC垃圾回收演算法-標記清除法、複製演算法、標記壓縮法、分代演算法

[1] HandlePromotionFailure:是一種相對於"判斷老年代剩餘空間必須大於新生代所有物件佔用記憶體綜合"策略更為冒進的一種策略,由於每一次晉升到老年代物件所需要的記憶體是不一定的,所以如果這個引數開啟,會取每一次晉升物件佔用記憶體的平均值作為參照,如果剩餘空間大於平均值,就不用執行Full GC。


歡迎關注我的微信公眾號

公眾號

相關文章