Java虛擬機器系列之垃圾回收機制(2)

林花鹿發表於2018-06-26

前言

上篇文章我們講解到了什麼是垃圾回收機制,如何判斷哪些物件要被回收以及垃圾收集演算法,而且我們也留了疑問:“為啥‘複製’演算法那麼浪費空間,而新生代還是選擇了它”,今天這篇文章,我們來講解。

一、JVM中記憶體分配策略

物件的記憶體分配,往大方向講,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配。少數情況下也可能直接分配在老年代中,分配的規則不是百分百固定的,其細節取決於當前使用的是哪一種垃圾回收器組合,還有虛擬機器中與記憶體相關的引數的設定。

新生代和老年代的大致結構:

  • 新生代:Eden、From Survivor、To Survivor
  • 老年代
    新生代老年代結構圖

JVM物件分配機制

  • 物件優先在Eden分配。當Eden區沒有足夠的空間進行分配時,虛擬機器就會發動一次新生代的垃圾回收動作(Minor GC)。

當使用引數 -XX:SurvivorRatio=8 決定了新生代中Eden區與一個Survivor區的空間比例是8:1,這裡要注意,是 一個 Survivor區的比例,而不是總的Survivor區的比例,而且新生代總用空間是Eden區+1個Survivor區的總容量

  • 大物件直接進入老年代

大物件指大量連續記憶體空間的Java物件,比如那種很長的字串以及陣列

在JVM中提供了—XX:PretenureSizeThreshold,令大於這個設定值的物件直接在老年代分配,這樣做的目的是避免Eden區及兩個Survivor區之前發生大量的記憶體複製。

二、JVM如何用分代收集的思想管理記憶體?

虛擬機器會給每一個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將其移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區每“熬過”一次Minor GC,年齡就會增加1歲。當它達到一定的閾值(預設為15歲),就將會晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定

然而在JVM中為了更好的適應不同的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTeuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無序等到MaxTeuringThreshold所要求的年齡。

下面來圖解JVM記憶體管理大概的流程:

JVM記憶體管理大致流程

注意:哪個是From Survivor區和To Survivor區是根據當時處理情況而決定的,兩個Survivor區輪流充當最後存活物件的儲存空間,所以在一個Minor GC中,誰是最終存活物件的儲存空間,誰就是To Survivor區

從上圖我們就可以看出,為啥新生代是可以使用複製演算法的,首先Java物件大多數是具備朝生朝滅的,大多數的物件都會被回收,要複製的物件較少,而且我們複製演算法每次浪費利的空間是一個Survivor區,佔的比例是比較小。

三、如果新生代存活物件過多,一個Survivor區容納不下,怎麼辦?

空間分配擔保

由於我們只有一個Survivor區來做輪換,如果容納不下了,則會將無法容納的物件直接進入老年代,因此就有了以下的判斷流程:

每次發生Minor GC之前,虛擬機器會先檢查老年代最大可用空間是否大於新生代所有物件總空間,具體流程如下:
- 老年代最大可用空間是否大於新生代所有物件總空間
    - 大於:Minor GC是確保安全的
    - 小於:JVM則會檢視HandlePromotionFailure設定值是否允許擔保失敗
        - 不允許:則進行一次Full GC //step A
        - 允  許:那麼會繼續檢查老年代最大可用空間是否大於歷次晉升到老年代物件的平均大小 //step B
            - 大於:則進行一次Minor GC(這是一次冒險)//step 1
            - 小於:則進行一次Full GC
複製程式碼

首先,我們解釋下什麼是Minor GC和Full GC以及他們之間的區別

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝夕朝滅的特性,所以Minor GC非常頻繁,一般回收速度也快
  • 老年代GC(Full GC/Major GC):指發生在老年代的GC,出現了Major GC,經常會伴隨著至少一次Minor GC(非絕對的)。Full GC的速度一般比Minor GC的速度慢10倍以上。

看看上面的流程,為啥step1那裡是一次冒險,為啥會進行Full GC?

  • 為啥step1是冒險:因為當時老年代不能確保MinorGC是安全的,而要不要進行MinorGC的直接依據的是歷次晉升到老年代物件的平均大小,這裡有概率的原理。
  • 為啥會進行Full GC:這樣是增加老年代的可用空間,能更好的進行空間分配擔保,而如果Full GC後還是不夠,則會丟擲OutOfMemoryError。

JDK6 Update24後,HandlePromotionFailure的變化

JDK update24之後,HandlePromotionFailure引數已經不會影響到虛擬機器的空間分配擔保策略了,雖然還是定義了,但是並沒有使用,而是變成了只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小,就會進行Minor GC,否則就將進行Full GC,可以理解為上面的流程中step A被堵死了,必須走step B過。


如果有什麼錯誤、建議或者討論的,可以留言,互相學習進步,謝謝

參考資料

《深入理解Java虛擬機器》

相關文章