jvm內存分配與回收策略乾貨

概述

java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決兩個問題:給對象分配內存以及回收分配給對象的內存。

對象的內存分配,往大方向將,就是堆上分配(但也可能經過JIT編譯後被拆散爲標量類型並間接地棧上分配),對象主要分配在新生代的Eden區上,如果啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。少數情況也可能會直接分配在老年代中,分配的規則並不是百分之百固定,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中內存相關的參數的設置。

Minor GC和Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因爲java對象大多數都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
  • 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

對象優先在Eden分配

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

虛擬機提供了 -XX:+PrintGCDetails這個收集器日誌參數,高數虛擬機在發生垃圾收集行爲時打印內存回收日誌,並且在進程退出的時候輸出當前的內存各區域分配情況。在實際應用中,內存回收日誌一般是打印到文件後通過日誌工具進行分析。

大對象直接進入老年代

大對象

指需要大量連續內存空間的java對象,最典型的大對象就是那種很長的字符串以及數組(例如byte[ ]數組就是典型的大對象),大對象對虛擬機的內存分配來說就是一個壞消息(比遇到一個大對象更加壞的消息就是遇到一羣“朝生夕滅”的大對象),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(新生代採用複製算法收集內存)。

注意:PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器一般並不需要設置。如果遇到必須使用此參數的場合,可以考慮ParNew加CMS的收集器組合。

長期存活的對象將進入老年代

虛擬機給每個對象定義了一個對象年齡(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存活後的對象突增,遠遠高於平均值的話,依然會導致擔保失敗。如果出現擔保失敗,那就只好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。

注意:在JDK 6 Update 24之後,這個測試結果會有差異,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用了。JDK 6 Update 24之後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

–摘自《深入理解java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章