聊聊JVM的垃圾回收機制GC

曾燕輝發表於2018-06-25

1. jvm結構圖

hotspot jvm結構如下(虛擬機器棧和本地方法棧合一起了):



2. jvm為什麼分新生代和老年代

我們先來屢屢,為什麼需要把堆分代?不分代不能完成他所做的事情麼?其實不分代完全可以,分代的唯一理由就是優化GC效能。你先想想,如果沒有分代,那我們所有的物件都在一塊,GC的時候我們要找到哪些物件沒用,這樣就會對堆的所有區域進行掃描。而我們的很多物件都是朝生夕死的,如果分代的話,我們把新建立的物件放到某一地方,當GC的時候先把這塊存“朝生夕死”物件的區域進行回收,這樣就會騰出很大的空間出來。


3. 新生代中的物件如何進入老年代

HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。預設比例為8:1,為啥預設會是這個比例,接下來我們會聊到。一般情況下,新建立的物件都會被分配到Eden區(一些大物件特殊處理),這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。物件在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時預設15歲,可以通過引數-XXMaxTenuringThreshold設定,就會被移動到年老代中。

因為年輕代中的物件基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收演算法使用的是複製演算法,複製演算法的基本思想就是將記憶體分為兩塊,每次只用其中一塊,當這一塊記憶體用完,就將還活著的物件複製到另外一塊上面。複製演算法不會產生記憶體碎片。

在GC開始的時候,物件只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到“To”,而在“From”區中,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有物件移動到年老代中。



4. 新生代和老年代GC

4.1 新生代GC(Minor GC)

指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

物件優先在Eden分配

大多數情況下,物件在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC。每進行一次Minor GC新生代中的物件的年齡+1。

Minor GC的時候,如下物件進入老年代:

大物件直接進入老年代

大物件即需要大量連續記憶體空間的Java物件,如長字串及陣列。經常出現大物件導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置他們。 
虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代分配。 這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(新生代採用複製演算法收集記憶體)。

長期存活的物件將進入老年代

虛擬機器給每個物件定義了一個物件年齡計數器,在物件在Eden建立並經過第一次Minor GC後仍然存活,並能被Suivivor容納的話,將會被移動到Survivor空間,並物件年齡設定為1。每經歷過Minor GC,年齡就增加1歲,當到一定程度(預設15歲,可以通過引數-XXMaxTenuringThreshold設定),就將會晉升年老代。

動態物件年齡判定

為了更好地適應不同程式記憶體狀況,虛擬機器並不硬性要求物件年齡達到MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入年老代。


4.2 老年代GC(Major GC/Full GC)

指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。 Major GC的速度一般會比Minor GC慢10倍以上。

空間分配擔保

在發生Minor GC之前,虛擬機器會先檢查年老代最大可用的連續空間是否大於新生代所有物件的總空間,如果條件成立,那麼Minor GC可以確保是安全的。

如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。

如果允許,那麼會繼續檢查年老代最大可用連續空間是否大於歷次晉升到年老代物件的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次Minor GC是有風險的。

如果小於,或者HandlePromotionFailure設定不允許冒險,那這時候改為進行一次Full GC。

下面解釋一下“冒險”是冒了什麼風險,新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量物件在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代。

與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些物件的剩餘空間,一共有多少物件會活下來在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代物件容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的物件突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。

如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。 雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關開啟,避免Full GC過於頻繁。


參考資料:

JDK8-廢棄永久代(PermGen)迎來元空間(Metaspace)

聊聊JVM的年輕代

JVM之新生代進入老年代

相關文章