天天看點

JVM進階之GC(五)對象的記憶體配置設定和回收政策

簡單回顧下JVM記憶體結構和垃圾回收器。

JVM記憶體結構

JVM記憶體主要由新生代、老年代、永久代構成。

  1. 新生代(Young Generation):大多數對象在新生代中被建立,其中很多對象的生命周期很短。每次新生代的垃圾回收(又稱Minor

    GC)後隻有少量對象存活,是以選用複制算法,隻需要少量的複制成本就可以完成回收。

    新生代内又分三個區:一個Eden區,兩個Survivor區(一般而言),大部分對象在Eden區中生成。當Eden區滿時,還存活的對象将被複制到兩個Survivor區(中的一個)。當這個Survivor區滿時,此區的存活且不滿足“晉升”條件的對象将被複制到另外一個Survivor區。對象每經曆一次Minor GC,年齡加1,達到“晉升年齡門檻值”後,被放到老年代,這個過程也稱為“晉升”。顯然,“晉升年齡門檻值”的大小直接影響着對象在新生代中的停留時間,在Serial和ParNew GC兩種回收器中,“晉升年齡門檻值”通過參數MaxTenuringThreshold設定,預設值為15。

  2. 老年代(Old Generation):在新生代中經曆了N次垃圾回收後仍然存活的對象,就會被放到年老代,該區域中對象存活率高。老年代的垃圾回收(又稱Major GC)通常使用“标記-清理”或“标記-整理”算法。整堆包括新生代和老年代的垃圾回收稱為Full GC(HotSpot VM裡,除了CMS之外,其它能收集老年代的GC都會同時收集整個GC堆,包括新生代)。
  3. 永久代(Perm Generation):主要存放中繼資料,例如Class、Method的元資訊,與垃圾回收要回收的Java對象關系不大。相對于新生代和年老代來說,該區域的劃分對垃圾回收影響比較小。

常見垃圾回收器

不同的垃圾回收器,适用于不同的場景。常用的垃圾回收器:

  1. 串行(Serial)回收器是單線程的一個回收器,簡單、易實作、效率高。
  2. 并行(ParNew)回收器是Serial的多線程版,可以充分的利用CPU資源,減少回收的時間。
  3. 吞吐量優先(Parallel Scavenge)回收器,側重于吞吐量的控制。
  4. 并發标記清除(CMS,Concurrent Mark Sweep)回收器是一種以擷取最短回收停頓時間為目标的回收器,該回收器是基于“标記-清除”算法實作的。

對象記憶體配置設定政策

對象的記憶體配置設定,就是在堆上配置設定(如果經過JIT編譯器逃逸分析,發現有些對象沒有逃逸出方法,那麼有可能堆記憶體配置設定會被優化成棧記憶體配置設定),對象主要配置設定在eden區,少數情況下也可能直接配置設定至老年代中,配置設定的規則視目前使用的垃圾收集器組合和記憶體參數規則決定。

1. 對象優先在eden配置設定

對象在絕大多數情況下,在新生代eden區配置設定,當eden區沒有足夠空間進行配置設定的時候,JVM會發起一次Minor GC。

相關記憶體參數如下:

  • -Xms:最小堆記憶體值
  • -Xmx:最大堆記憶體值
  • -Xmn:新生代記憶體值
  • -XX:SurvivorRatio:新生代中eden區與一個survivor區的空間比

比如,設定的參數是

-Xms20M

-Xmx20M

-Xmn10M

-XX:SurvivorRatio=8

,可得知,最小堆和最大堆記憶體一緻,即堆記憶體固定為20MB,新生代為10MB,而

老年代=堆記憶體-新生代

,得知老年代為10MB,eden區與survivor區的比例是8:1,

eden區=新生代 * SurvivorRatio / 10

,eden區的大小為8MB,survivor區為2MB,s0和s1區都為1MB,那麼新生代的總可用空間為9MB(

eden區 + 1個survivor區

)。來看測試代碼:

public class Test1 {
    private static final int _2MB =  *  * ;

    public static void main(String[] args) {
        byte[] object1 = new byte[_2MB];
        byte[] object2 = new byte[_2MB];
        byte[] object3 = new byte[_2MB];
        byte[] object4 = new byte[* _2MB];
    }

}
           

運作後GC日志如下:

JVM進階之GC(五)對象的記憶體配置設定和回收政策

不懂GC日志的請移步GC 日志。

從日志中得知,上述代碼發生了一次Minor GC,結果是新生代由7651K變為了529K,而總記憶體占用量基本沒變。這次GC的原因是給object4配置設定記憶體時,object1、object2、object3已存在eden區,并占用了6MB的記憶體,剩餘的空間不足以配置設定給object4的4MB,是以發生了Minor GC。

GC的時候JVM又發現3個2MB大小的對象都無法放入survivor區,是以隻能通過配置設定擔保機制提前轉移到了老年代。GC結束後,從日志中可看出tenured generation(老年代)used了6144K,def new generation(新生代)used 4791K,即object4所占用的記憶體。

2. 大對象直接進入老年代

大對象,即需要大量連續記憶體空間的對象,比如上述測試代碼中的byte數組就是典型的大對象。經常出現大對象就容易導緻記憶體還有不少空間時就提前觸發了GC,以便擷取更大的連續空間來配置設定。

虛拟機提供了一個參數

-XX:PretenureSizeThreshold

,大于此設定值的對象将直接進入老年代配置設定記憶體,這樣做的目的是避免在eden區和兩個survivor區之間發生大量的記憶體複制。

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

與大對象相對應,小對象在GC過程中通常不會因為記憶體空間不夠配置設定而直接進入老年代,而是通過給每個對象定義一個對象年齡計數器的方式。對象在eden區出生,經過第一次Minor GC後仍然能存活,并且能被survivor區容納,将被移動到survivor區中,并且對象的年齡設為1。對象在survivor區每經過一次Minor GC,對象的年齡就加1歲,當它的年齡增加到一定程度時(預設為15歲),就會晉升到老年代中去。

對象晉升老年代的年齡門檻值,可通過參數

-XX:MaxTenuringThreshold

調整。

4. 動态對象年齡判定

為了能更好地适應不同程式的記憶體狀況,虛拟機并不是永遠的要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,而是有個機智的政策:如果在survivor區中相同年齡的所有對象大小的總和大于survivor區空間的一半時,年齡大于或等于該年齡的對象就可以直接進入老年代,無須達到MaxTenuringThreshold中要求的年齡。

5. 空間配置設定擔保

在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象的空間,如果條件滿足,那麼Minor GC就是安全的,否則繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,則“嘗試”進行一次Minor GC,如果小于,則要進行Full GC。

為什麼是嘗試進行Minor GC呢?因為新生代采用複制收集算法,隻使用其中一個survivor空間來作為輪換備份,是以出現大量對象在Minor GC後仍然存活的情況下(最極端的就是記憶體回收後新生代中所有對象都存活),就需要老年代進行配置設定擔保,把survivor區無法容納的對象直接移至老年代。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的空間,然而一共會有多少對象存活下來,在實際完成記憶體回收的過程中是無法明确知曉的,是以隻好取之前每一次回收晉升到老年代的對象容量的平均大小值作為參考值,與老年代的剩餘空間比較,來決定是否進行Full GC來讓老年代騰出更多空間。

總結

  1. 對象優先在eden區配置設定記憶體,如果eden沒有足夠的空間,則會觸發Minor GC,清理空間
  2. 對象達到了MaxTenuringThreshold設定的年齡,或survivor區中相同年齡的所有對象大小的總和大于survivor區空間的一半時,年齡大于或等于該年齡的對象,就可以直接進入老年代
  3. 新生代對象的總大小或者曆次晉升的平均大小大于老年代的連續空間時,就會進行Full GC,反之進行Minor GC

最後補充幾點會觸發Full GC的方式:

  1. Perm(永久代)空間不足;
  2. CMS GC時出現promotion failed和concurrent mode failure(concurrent mode

    failure發生的原因一般是CMS正在進行,但是由于老年代空間不足,需要盡快回收老年代裡面的不再被使用的對象,這時停止所有的線程,同時終止CMS,直接進行Serial

    Old GC);

  3. 統計得到的Minor GC晉升到老年代的平均大小大于老年代的剩餘空間;
  4. 主動觸發Full GC(執行jmap -histo:live [pid])來避免碎片問題。