1. 程式人生 > >Java虛擬機器6:記憶體分配策略

Java虛擬機器6:記憶體分配策略

大物件直接進入老年代Java的自動記憶體管理最終可以歸結為自動化地解決了兩個問題:

  • 給物件分配記憶體
  • 回收分配給物件的記憶體

     物件的記憶體分配通常是在堆上分配(除此以外還有可能經過JIT編譯後被拆散為標量型別並間接地棧上分配),物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB(Thread Local Allocation Buffer)上分配。少數情況下也可能會直接分配在老年代中,分配的規則並不是固定的,實際取決於垃圾收集器的具體組合以及虛擬機器中與記憶體相關的引數的設定。

物件優先在Eden區分配

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

大物件直接進入老年代

       所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是很長的字串以及陣列。大物件對虛擬機器的記憶體分配來說是一個壞訊息(尤其是遇到朝生夕滅的“短命大物件”,寫程式時應避免),經常出現大物件容易導致記憶體還有不少空間時就提前觸發GC以獲取足夠的連續空間來安置它們

    虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(新生代採用複製演算法回收記憶體)。

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

      既然虛擬機器採用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識別哪些物件應放在新生代,哪些物件應放在老年代中。為了做到這點,虛擬機器給每個物件定義了一個物件年齡(Age)計數器

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

動態物件年齡判定

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

,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

     在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC

     前面提到過,新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量物件在Minor GC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些物件的剩餘空間,一共有多少物件會活下來在實際完成記憶體回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代物件容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

     取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活後的物件突增,遠遠高於平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關開啟,避免Full GC過於頻繁。

Full GC的觸發條件

   對於Minor GC,其觸發條件非常簡單,當Eden區空間滿時,就將觸發一次Minor GC。而Full GC則相對複雜,因此本節我們主要介紹Full GC的觸發條件。

   呼叫System.gc()

      此方法的呼叫是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。因此強烈建議能不使用此方法就不要使用,讓虛擬機器自己去管理它的記憶體,可通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc()。

老年代空間不足

     老年代空間不足的常見場景為前文所講的大物件直接進入老年代長期存活的物件進入老年代等,當執行Full GC後空間仍然不足,則丟擲如下錯誤: Java.lang.OutOfMemoryError: Java heap space為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列

空間分配擔保失敗

   前文介紹過,使用複製演算法的Minor GC需要老年代的記憶體空間作擔保,如果出現了HandlePromotionFailure擔保失敗,則會觸發Full GC。

JDK 1.7及以前的永久代空間不足

       在JDK 1.7及以前,HotSpot虛擬機器中的方法區是用永久代實現的,永久代中存放的為一些class的資訊、常量、靜態變數等資料,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊: java.lang.OutOfMemoryError: PermGen space 為避免PermGen佔滿造成Full GC現象,可採用的方法為增大PermGen空間或轉為使用CMS GC。

在JDK 1.8中用元空間替換了永久代作為方法區的實現,元空間是本地記憶體,因此減少了一種Full GC觸發的可能性。

Concurrent Mode Failure

    執行CMS GC的過程中同時有物件要放入老年代,而此時老年代空間不足(有時候“空間不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC),便會報Concurrent Mode Failure錯誤,並觸發Full GC。