【深入理解JVM】:內存分配與回收策略

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

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

本文中的內存分配策略指的是Serial / Serial Old收集器下(ParNew / Serial Old收集器組合的規則也基本一致)的內存分配和回收的策略。

對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。虛擬機的參數-XX:PrintGCDetails可以打印內存回收日誌。

代碼示例:

private static final int _1MB = 1024 * 1024;  
/**  
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
*/  
public static void testAllocation() {  
    byte[] allocation1, allocation2, allocation3, allocation4;  
    allocation1 = new byte[2 * _1MB];  
    allocation2 = new byte[2 * _1MB]; 
    allocation3 = new byte[2 * _1MB]; 
    allocation4 = new byte[4 * _1MB];  // 出現一次Minor GC  
}

運行結果:

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)  
eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000)  
to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)  
 the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)  
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
 the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
No shared spaces configured.

代碼嘗試分配3個2MB大小和1個4MB大小的對象,在運行時通過-Xms20M、 -Xmx20M、 -Xmn10M這3個參數限制了Java堆大小爲20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8∶1,從輸出的結果也可以清晰地看到eden space 8192K、from space 1024K、to space 1024K的信息,新生代總可用空間爲9216KB(Eden區+1個Survivor區的總容量)。

執行testAllocation()中分配allocation4對象的語句時會發生一次Minor GC,這次GC的結果是新生代6651KB變爲148KB,而總內存佔用量則幾乎沒有減少(因爲allocation1、allocation2、allocation3三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。這次GC發生的原因是給allocation4分配內存的時候,發現Eden已經被佔用了6MB,剩餘空間已不足以分配allocation4所需的4MB內存,因此發生Minor GC。GC期間虛擬機又發現已有的3個2MB大小的對象全部無法放入Survivor空間(Survivor空間只有1MB大小),所以只好通過分配擔保機制提前轉移到老年代去。

這次GC結束後,4MB的allocation4對象順利分配在Eden中,因此程序執行完的結果是Eden佔用4MB(被allocation4佔用),Survivor空閒,老年代被佔用6MB(被allocation1、allocation2、allocation3佔用)。通過GC日誌可以證實這一點。

大對象直接進入老年代

所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。

大對象對虛擬機的內存分配來說就是一個壞消息(比遇到一個大對象更加壞的消息就是遇到一羣“朝生夕滅”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

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

代碼示例:

private static final int _1MB = 1024 * 1024;   
/**  
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
* -XX:PretenureSizeThreshold=3145728 
*/  
public static void testPretenureSizeThreshold() {  
    byte[] allocation;  
    allocation = new byte[4 * _1MB];  //直接分配在老年代中  
}

運行結果:

Heap  
    def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)  
    eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)  
    from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)  
    No shared spaces configured.

執行代碼中的testPretenureSizeThreshold()方法後,我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接就分配在老年代中,這是因爲PretenureSizeThreshold被設置爲3MB(就是3145728,這個參數不能像-Xmx之類的參數一樣直接寫3MB),因此超過3MB的對象都會直接在老年代進行分配。

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

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

爲了在內存回收時能識別哪些對象應放在新生代,哪些對象應放在老年代中。虛擬機給每個對象定義了一個對象年齡(Age)計數器。

對象年齡的判定:

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

代碼示例:

    private static final int _1MB = 1024 * 1024;  
    /**  
    * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 
    * -XX:+PrintTenuringDistribution  
    */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold() {  
        byte[] allocation1, allocation2, allocation3;  
        allocation1 = new byte[_1MB / 4];    
         // 什麼時候進入老年代取決於XX:MaxTenuringThreshold設置  
        allocation2 = new byte[4 * _1MB];  
        allocation3 = new byte[4 * _1MB];  
        allocation3 = null;  
        allocation3 = new byte[4 * _1MB];  
}

以MaxTenuringThreshold=1參數來運行的結果:

[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 1 (max 1)  
- age   1:     414664 bytes,     414664 total  
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]   
[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 1 (max 1)  
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
    def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
    eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.

以MaxTenuringThreshold=15參數來運行的結果:

[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
- age   1:     414664 bytes,     414664 total  
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
- age   2:     414520 bytes,     414520 total  
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
    def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)  
    eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.

此方法中的allocation1對象需要256KB內存,Survivor空間可以容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後非常乾淨地變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代Survivor空間,這時新生代仍然有404KB被佔用。

動態對象年齡判定

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

動態對象年齡判定:

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

代碼示例:

    private static final int _1MB = 1024 * 1024;  
/**  
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
* -XX:+PrintTenuringDistribution  
*/  
@SuppressWarnings("unused")  
public static void testTenuringThreshold2() { 
    byte[] allocation1, allocation2, allocation3, allocation4;  
    allocation1 = new byte[_1MB / 4];   
      // allocation1+allocation2大於survivo空間一半  
    allocation2 = new byte[_1MB / 4];    
    allocation3 = new byte[4 * _1MB];  
    allocation4 = new byte[4 * _1MB];  
    allocation4 = null;  
    allocation4 = new byte[4 * _1MB];  
}

運行結果:

[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 1 (max 15)  
- age   1:     676824 bytes,     676824 total  
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]   
[GC [DefNew  
Desired Survivor size 524288 bytes, new threshold 15 (max 15)  
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
    def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
    eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.

執行代碼中的testTenuringThreshold2()方法,並設置-XX:MaxTenuringThreshold=15,會發現運行結果中Survivor的空間佔用仍然爲0%,而老年代比預期增加了6%,也就是說,allocation1、allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。因爲這兩個對象加起來已經到達了512KB,並且它們是同年的,滿足同年對象達到Survivor空間的一半規則。我們只要註釋掉其中一個對象new操作,就會發現另外一個就不會晉升到老年代中去了。

空間分配擔保

在發生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過於頻繁。

參考
1、周志明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社

發佈了117 篇原創文章 · 獲贊 184 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章