深入理解Java虛擬機第三章讀書筆記:內存分配與回收策略

Java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決了兩個問題:

  1. 給對象分配內存
  2. 回收分配給對象的內存

對象的內存分配,從大體上講,就是在堆內存中進行分配,對象主要是分配在新生代的Eden區上,如果啓動了本地線程分配緩衝(TLAB),則將按線程優先在TLAB上分配。少數情況也會直接分配在老年代中,分配的規則不是百分之百固定的,細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

一下是幾條最普遍的內存分配規則。

1.對象優先在Eden分配

大多數情況下,對象在新生代的Eden區中分配。當Eden區中沒有足夠的內存空間進行分配時,虛擬機將發起一次MinorGC(新生代GC,分配率越高,MinorGC越頻繁,但是回收速度一般也比較快)。

測試代碼:

private static final int_1MB = 1024 * 1024;

/**
 * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -xx:+PrintGCDetails -xx:SurvivorRatio=8
*/

public static void testAllocataion(){
    byte[] allocation,allocation2,allocation3,allocation4;
    allocation 1 = new byte[2 * _1MB];
    allocation 2 = new byte[2 * _1MB];
    allocation 3 = new byte[2 * _1MB];
    allocation 4 = new byte[4 * _1MB];        //出現一處MinorGC
}

實驗參數設置:

-Xms20M   -Xmx20M    -Xmn10M這三個參數限制了Java堆的大小爲20M且不可擴展,其中10M分給新生代,10M分配給來年代

-xx:SurvivorRatio=8 確定了新生代中Eden區與Survivor區的空間比例是8:1(新生代中有兩個Survivor區)

實驗結果:

testAllocation()方法執行,在分配allocation4對象時會觸發一次MinorGC,原因是在給allocation4分配對象時候,發現Eden區域已經被佔用了6MB,剩餘空間無法容納allocation4對象的4MB內存。GC期間虛擬機又發現已經分配的3個大小爲2MB的對象全部無法放入Survivor空間中(因爲Survivor空間只有1MB),所以只能通過分配擔保機制將這三個對象提前轉移到老年代中,而MinorGC後allocation4對象將分配到Eden區域。

GC結束後內存分配情況:Eden佔用4MB(allocation4),Survivor空閒,老年代佔用6MB(allocation1、2、3)

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

所謂的大對象就是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(上面代碼中的byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來說是一個壞消息(特別是“朝生夕滅”的“短命大對象”,寫程序時應該避免),經常出現大對象容易導致內存還有不少空間時就要提前觸發垃圾收集以獲取足夠的連續空間來安置它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,目的是當分配大於這個值的對象時,直接在老年代中分配。這樣做的目的是避免在Eden區以及兩個Survivor區之間發生大量的內存複製。

Q:爲什麼新生代中要設置兩個Survivor區?

A:因爲要保證任何時候都有一個Survivor區是空的,避免導致出現內存碎片而浪費了內存空間。觸發MinorGC時,會先把存活的對象從Eden區複製到Survivor區,然後再清空Eden區的空間。如果只有一個Survivor區的話,那麼觸發第一次MinorGC時,存活對象複製到Survivor,Eden清空,觸發第二次MinorGC時,存活對象又從Eden轉移到Survivor,那麼這兩次複製過來的對象內存地址肯定是不連續的,就會產生內存碎片,導致空間浪費。如果有兩個Survivor區的話,任何時候保持一個Survivor是空的,就不會造成這種情況。(只有經歷了16次MinorGC還存活的對象纔會從Survivor區送到老年代

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

既然虛擬機採用了分代手機的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代。爲了做到這點,虛擬機給每個對象定義了一個對象年齡計數器。

如果對象在Eden中出生並經過第一次MinorGC後任然可以存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且設置對象年齡爲1歲,對象在Survivor區中沒熬過一次MinorGC,年齡就會增加一歲,當他年齡增加到一定程度時(默認爲15歲),就會被晉升到老年代中。對象晉升到老年代的閥值可以通過參數-XX:MaxTenuringThreshold設置。 

4、動態對象年齡判定

爲了更好的適應不同程序的內存狀況,虛擬機並不是永遠的要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代。

如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間大小的一半,年齡大於或等於該年齡的對象就會直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。

5、空間分配擔保

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

新生代使用的是複製收集算法,爲了保證內存空間的利用率,每次只能使用一塊Survivor空間,因此當出現大量對象在MinorGC後依然存活,而Survivor空間不足夠存放這些對象時,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。這與生活中的貸款擔保相似,老年代要進行擔保,那麼就要確保老年代的內存空間大小足夠去容納這些對象。

有多少對象會活下來在實際完成前是無法明確得知的,所以只好取之前每一次晉升到老年代中的所有對象大小的平均值,來與老年代的剩餘空間進行比較,從而決定是進行Full GC來讓老年代騰出更多的空間。

取平均值並不是每一次都會成功,有可能這一次存活下來的對象所佔內存空間遠超這個平均值,那麼依然會導致擔保失敗,擔保失敗以後會重新發起一次Full GC。雖然擔保失敗會繞很大一個圈子,但是爲了避免Full GC太頻繁,大部分情況下還是會把HandlePromotionFailure開關打開。

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