《深刻理解Java虛擬機》內存分配策略

 

上節學習回顧算法

 

一、判斷對象存活算法:引用計數法和可行性分析算法數組

二、垃圾收集算法:標記-清除算法、複製算法、標記-整理算法安全

三、垃圾收集器:多線程

Serial:新生代收集器,採用複製算法,單線程。學習

ParNew:新生代收集器,採用複製算法,多線程。測試

Parallel Scavenge:新生代收集器,採用複製算法,多線程,注重吞吐量。spa

Serial Old:老年代收集器,採用標記-整理算法,單線程。線程

Parallel Old:老年代收集器,採用標記-整理算法,多線程,與Parallel Scavenge結合使用。日誌

CMS:老年代收集器,採用標記-清除算法,相比以上收集器收集過去相對複雜,中止時間短。code

G1:年輕代和老年代收集器,基本採用標記-整理算法,局部採用複製算法,收集過程跟CMS至關,但概念差別,是目前最新的收集器之一,使用範圍暫時有待檢驗。

 

本節學習重點

 

本節主要經過測試代碼來學習堆中對象的內存分配和回收策略,期間會經過打印GC和內存分配的日誌來分析。首先,經過下圖先熟悉一下對的內存分配區域圖:

從上一節的學習和圖中能夠知道,目前新生代收集算法採用的都是複製算法,複製算法把新生代內存分爲一個較大的Eden空間和兩個較小的Survivor空間。HotSpot默認的分配比例是8:1:1.,例如新生代一共分配10MB,那麼Eden佔用8MB,而兩個Survivor各佔1MB。

注:如下全部測試例子的都是基於JDK8進行測試,默認使用的是Parallel Scavenge/Parallel Old收集器組合,爲了跟書本保持一致,我下面會使用-XX:+UseSerialGC參數切換回Serial收集器,由於可能不一樣收集器且不用JDK版本可能會有不同的狀況。

 

 

  • 對象優先在Eden分配

 

大多數狀況下,對象在新生代Eden去中分配,但Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。接下來,咱們經過-XX:PrintGCDetails參數打印測試代碼執行日誌進行詳細分析。

 

測試代碼:

    private static final int _1MB = 1024*1024; /** * VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -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]; }

 

執行結果:

[GC (Allocation Failure) [DefNew: 6824K->268K(9216K), 0.0087400 secs] 6824K->6412K(19456K), 0.0087927 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] Heap def new generation   total 9216K, used 4446K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 26% used [0x00000000ff500000, 0x00000000ff543018, 0x00000000ff600000) to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000) Metaspace used 2876K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

 

狀況分析:

先對GC的日誌說明一下,有「[GC……]」字眼的就是GC日誌記錄,而Heap如下的信息是JVM關閉前的堆使用狀況信息描述。其中GC表明MinorGC,若是是Full GC的話會直接寫着Full GC,[DefNew: 6824K->268K(9216K), 0.0087400 secs]中DefNew表明使用的是表明Serial收集器,6824K->268K(9216K)中6284K表明新生代收集前使用內存,268K表明收集後的使用內存,而9216表明新生代的總分配內存,0.0087400 secs爲新生代收集時間。外層的6824K->6412K(19456K)表明整個Java堆的收集前使用內存->收集後使用內存(總分配內存),0.0087927 secs爲整個GC的收集時間。整串GC日誌都是類Json格式,比較容易看。

 

在上述測試例子中,我把JVM設置了不可擴展內存20MB,其中新生代10MB,老年代10MB,而新生代區域的分配比例是8:1:1,使用Serial/Serial Old組合收集器。從代碼能夠看出,allocation一、allocation二、allocation3一共須要6MB,而Eden一共有8MB,優先分配到Eden。但再分配allocation4的時候Eden空間不夠,執行了一次Minor GC,在GC中,因爲Survivor只有1MB,不夠存放allocation一、allocation二、allocation3,因此直接遷移到老年代了,最後Eden空閒出來了就能夠放allocation4了。最後經過Heap打印信息能夠看到JVM內存分配的最後狀態,「def new generation   total 9216K, used 4446K」爲allocation4最後分配新生代所佔用的4MB,而「tenured generation   total 10240K, used 6144K」則是老年代被allocation一、allocation二、allocation3所佔用的6MB了。

 

 

  • 大對象直接進入老年代

 

所謂大對象是指須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(例如上面例子的byte[]數組)。大對象對虛擬機內存分配來講是一個壞消息,常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來「安置」它們。虛擬機提供了一個-XX:PretenureSizeThreshold參數來設置大對象的界限,大於此值則直接分配在老年代去了。下面用代碼測試一下吧。

 

測試代碼:

    private static final int _1MB = 1024*1024; /** * VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:+PretenureSizeThreshold=3145728 */
    public static void testAllocation(){ byte[] allocation1; allocation1 = new byte[4 * _1MB]; }

 

執行結果:

Heap def new generation   total 9216K, used 844K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd30a8, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000) Metaspace used 2875K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

 

狀況分析:

從以上打印JVM堆信息能夠看出,allocation1分配的是4MB,大於PretenureSizeThreshold定義的3MB閥值,因此直接分配到老年代去了。

 

 

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

 

因爲Minor GC跟Full GC是差異的,Minor的主要對象仍是新生代,對象在Minor後並不都會直接進入老年代,除非Survivor空間不夠,不然此存活對象會通過屢次Minor GC後還生存的話才進入老年代,而虛擬機默認的Minor GC次數爲15次,可經過-XX:MaxTenuringThreshold進行次數設置。

 

測試代碼:

private static final int _1MB = 1024*1024; /** * VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:MaxTenuringThreshold=15 OR 1 */
    public static void testAllocation(){ byte[] allocation1,allocation2,allocation3; allocation1 = new byte[1 * _1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; }

 

執行結果:

MaxTenuringThreshold = 15

[GC (Allocation Failure) [DefNew: 5031K->524K(9216K), 0.0132760 secs] 5031K->4620K(19456K), 0.0133385 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] [GC (Allocation Failure) [DefNew: 4620K->0K(9216K), 0.0007228 secs] 8716K->4614K(19456K), 0.0007564 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4614K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa81ac0, 0x00000000ffa81c00, 0x0000000100000000) Metaspace used 2876K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

MaxTenuringThreshold = 1

[GC (Allocation Failure) [DefNew: 5031K->524K(9216K), 0.0065023 secs] 5031K->4620K(19456K), 0.0065518 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] [GC (Allocation Failure) [DefNew: 4620K->0K(9216K), 0.0013012 secs] 8716K->4614K(19456K), 0.0013644 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4614K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa81ac0, 0x00000000ffa81c00, 0x0000000100000000) Metaspace used 2876K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

 

狀況分析:

從以上輸出信息能夠看到,不管是MaxTenuringThreshold=15仍是1,執行結果都是同樣的,至少跟書本描述的不一致,我懷疑是由於JDK版本不用而機制有所差別,我馬上切換到JDK6下執行一樣的操做以下:

MaxTenuringThreshold = 15

[GC [DefNew: 4679K->375K(9216K), 0.0044310 secs] 4679K->4471K(19456K), 0.0044650 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 4635K->375K(9216K), 0.0086340 secs] 8731K->4471K(19456K), 0.0086660 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] Heap def new generation   total 9216K, used 4635K [0xee330000, 0xeed30000, 0xeed30000) eden space 8192K, 52% used [0xee330000, 0xee758fe0, 0xeeb30000) from space 1024K, 36% used [0xeeb30000, 0xeeb8dc68, 0xeec30000) to space 1024K, 0% used [0xeec30000, 0xeec30000, 0xeed30000) tenured generation total 10240K, used 4096K [0xeed30000, 0xef730000, 0xef730000) the space 10240K, 40% used [0xeed30000, 0xef130010, 0xef130200, 0xef730000) compacting perm gen total 16384K, used 1912K [0xef730000, 0xf0730000, 0xf3730000) the space 16384K, 11% used [0xef730000, 0xef90e3b8, 0xef90e400, 0xf0730000) No shared spaces configured.

MaxTenuringThreshold = 1

[GC [DefNew: 4679K->375K(9216K), 0.0037650 secs] 4679K->4471K(19456K), 0.0037960 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 4471K->0K(9216K), 0.0010150 secs] 8567K->4471K(19456K), 0.0010580 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation   total 9216K, used 4423K [0xee350000, 0xeed50000, 0xeed50000) eden space 8192K, 54% used [0xee350000, 0xee7a1fa8, 0xeeb50000) from space 1024K, 0% used [0xeeb50000, 0xeeb50000, 0xeec50000) to space 1024K, 0% used [0xeec50000, 0xeec50000, 0xeed50000) tenured generation total 10240K, used 4471K [0xeed50000, 0xef750000, 0xef750000) the space 10240K, 43% used [0xeed50000, 0xef1adc50, 0xef1ade00, 0xef750000) compacting perm gen total 16384K, used 1912K [0xef750000, 0xf0750000, 0xf3750000) the space 16384K, 11% used [0xef750000, 0xef92e3b8, 0xef92e400, 0xf0750000) No shared spaces configured.

以上是JDK6環境下的輸出,確實看到了差別,當MaxTenuringThreshold=15時,allocation1還停留在Survivor中,當MaxTenuringThreshold=1時,在Minor GC時就被遷移到老年代去了。看了JDK版本所作調整確實有所差別,具體的差別細節,可能後續還要進一步去了解。

 

 

  • 動態對象年齡斷定

 

爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象直接能夠進入老年代,無須等到MaxTenuringThreshold中要求的年齡。下面經過測試代碼對allocation2進行註釋前和註釋後的收集狀況進行對比。

 

測試代碼:

    private static final int _1MB = 1024*1024; /** * VM參數:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 */
    public static void testAllocation(){ byte[] allocation1,allocation2,allocation3,allocation4; allocation1 = new byte[1 * _1MB / 4]; allocation2 = new byte[2 * _1MB / 4];//註釋先後對比
        allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation3 = null; allocation4 = new byte[4 * _1MB]; }

 

執行結果:

allocation2被註釋的狀況下

[GC (Allocation Failure) [DefNew: 5031K->524K(9216K), 0.0515256 secs] 5031K->4620K(19456K), 0.0515942 secs] [Times: user=0.00 sys=0.05, real=0.05 secs] [GC (Allocation Failure) [DefNew: 4620K->0K(9216K), 0.0928683 secs] 8716K->8716K(19456K), 0.0929016 secs] [Times: user=0.00 sys=0.10, real=0.09 secs] Heap def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 8716K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe83040, 0x00000000ffe83200, 0x0000000100000000) Metaspace used 2876K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

allocation2沒有註釋的狀況下

[GC (Allocation Failure) [DefNew: 5379K->1023K(9216K), 0.0082513 secs] 5379K->5132K(19456K), 0.0083072 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] [GC (Allocation Failure) [DefNew: 5120K->0K(9216K), 0.0078267 secs] 9228K->9227K(19456K), 0.0078778 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] Heap def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 9227K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 90% used [0x00000000ff600000, 0x00000000fff02fd8, 0x00000000fff03000, 0x0000000100000000) Metaspace used 2875K, capacity 4486K, committed 4864K, reserved 1056768K class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

 

狀況分析:

從以上打印信息能夠看到,代碼執行並無想上文描述的那樣判斷Survivor的空間是否被同齡對象佔據一半就遷移到老年代,而是像上一個測試那樣,並無通過Survivor而都直接進入老年代了,這多是JDK8對收集規則的一些改進,我繼續嘗試使用JDK6再測試一次:

allocation2被註釋的狀況下

[GC [DefNew: 4679K->375K(9216K), 0.0033230 secs] 4679K->4471K(19456K), 0.0033590 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] [GC [DefNew: 4471K->375K(9216K), 0.0145510 secs] 8567K->8567K(19456K), 0.0145810 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap def new generation   total 9216K, used 4798K [0xee380000, 0xeed80000, 0xeed80000) eden space 8192K, 54% used [0xee380000, 0xee7d1fa8, 0xeeb80000) from space 1024K, 36% used [0xeeb80000, 0xeebddc40, 0xeec80000) to space 1024K, 0% used [0xeec80000, 0xeec80000, 0xeed80000) tenured generation total 10240K, used 8192K [0xeed80000, 0xef780000, 0xef780000) the space 10240K, 80% used [0xeed80000, 0xef580020, 0xef580200, 0xef780000) compacting perm gen total 16384K, used 1912K [0xef780000, 0xf0780000, 0xf3780000) the space 16384K, 11% used [0xef780000, 0xef95e3b8, 0xef95e400, 0xf0780000) No shared spaces configured.

allocation2沒有註釋的狀況下

[GC [DefNew: 5191K->887K(9216K), 0.0046370 secs] 5191K->4983K(19456K), 0.0048820 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC [DefNew: 4983K->0K(9216K), 0.0038680 secs] 9079K->9079K(19456K), 0.0039040 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation   total 9216K, used 4423K [0xee3c0000, 0xeedc0000, 0xeedc0000) eden space 8192K, 54% used [0xee3c0000, 0xee811fa8, 0xeebc0000) from space 1024K, 0% used [0xeebc0000, 0xeebc0000, 0xeecc0000) to space 1024K, 0% used [0xeecc0000, 0xeecc0000, 0xeedc0000) tenured generation total 10240K, used 9079K [0xeedc0000, 0xef7c0000, 0xef7c0000) the space 10240K, 88% used [0xeedc0000, 0xef69dc70, 0xef69de00, 0xef7c0000) compacting perm gen total 16384K, used 1912K [0xef7c0000, 0xf07c0000, 0xf37c0000) the space 16384K, 11% used [0xef7c0000, 0xef99e3e8, 0xef99e400, 0xf07c0000) No shared spaces configured.

經過以上JDK6執行的信息能夠看出,跟書本上描述的狀況一致,當只有allocation1佔據Survivor時,還不到一半空間,因此還停留在Survivor的空間。但當allocation2也存在時,執行第一次Minor GC的時候allocation一、allocation2應同時被遷移到Survivor,但allocation一、allocation2的總和已經達到了Survivor的一半,因此馬上被遷移到老年代了。狀況跟測試MaxTenuringThreshold參數的時候同樣,JDK8相對老版本JDK的處理方式仍是有差別的。

 

 

  • 空間分配擔保

 

在發生Minor GC前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象的空間,若是這個條件成立,那麼Minor GC能夠確保安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管這個Minor GC是有風險的;若是小於或者HandlePromotionFailure設置不容許冒險,那麼這時也要改成進行一次Full GC了。說白了就是虛擬機避免Full GC執行的次數而去作的檢查機制。

 

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

 

另外須要提醒,在JDK 6 Update 24以後,虛擬機已經再也不使用HandlePromotionFailure參數了,規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

 

 

  • 總結

 

本章節主要是經過測試用例對虛擬機收集規則的一下驗證,經過測試例子也可知,不一樣JDK版本的規則可能會有所改變,但垃圾收集的本質是不變的。不一樣狀況下虛擬機的收集器組合也是不同的,主要掌握了各類收集算法和收集器的狀況,就可根據實際狀況去是使用,使用過程當中也能夠根據本身掌握的知識經過具體的調節參數進行對比和調優,一切的前提仍是要知道垃圾收集是怎麼一回事。

相關文章
相關標籤/搜索