前言
對象的內存分配,往大的方向上講,就是在堆上分配,少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,其細節決定於當前使用的是哪種垃圾收集器組合,當然還有虛擬機中與內存相關的參數。垃圾收集器組合一般就是Serial+Serial Old和Parallel+Serial Old,前者是Client模式下的默認垃圾收集器組合,后者是Server模式下的默認垃圾收集器組合,文章使用對比學習法對比Client模式下和Server模式下同一條對象分配原則有什么區別。
TLAB
首先講講什么是TLAB。內存分配的動作,可以按照線程划分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程需要分配內存,就在哪個線程的TLAB上分配。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。這么做的目的之一,也是為了並發創建一個對象時,保證創建對象的線程安全性。TLAB比較小,直接在TLAB上分配內存的方式稱為快速分配方式,而TLAB大小不夠,導致內存被分配在Eden區的內存分配方式稱為慢速分配方式。
一、對象優先在Eden區分配
對象通常在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。
Minor GC:指發生在新生代的垃圾收集動作,非常頻繁,速度較快。
Major GC:指發生在老年代的GC,出現Major GC,經常會伴隨一次Minor GC,同時Minor GC也會引起Major GC,一般在GC日志中統稱為GC,不頻繁。
Full GC:指發生在老年代和新生代的GC,速度很慢,需要Stop The World。
來看下面一段代碼,虛擬機參數為“-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8”,即10M新生代,10M老年代,10M新生代中8M的Eden區,兩個Survivor區各1M。
public class EdenAllocationTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1 = new byte[2 * _1MB]; byte[] allocation2 = new byte[2 * _1MB]; byte[] allocation3 = new byte[2 * _1MB]; byte[] allocation4 = new byte[4 * _1MB]; } }
Client模式下:
[GC [DefNew: 6487K->194K(9216K), 0.0042856 secs] 6487K->6338K(19456K), 0.0043281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4454K [0x0000000005180000, 0x0000000005b80000, 0x0000000005b80000) eden space 8192K, 52% used [0x0000000005180000, 0x00000000055a9018, 0x0000000005980000) from space 1024K, 18% used [0x0000000005a80000, 0x0000000005ab0810, 0x0000000005b80000) to space 1024K, 0% used [0x0000000005980000, 0x0000000005980000, 0x0000000005a80000) tenured generation total 10240K, used 6144K [0x0000000005b80000, 0x0000000006580000, 0x0000000006580000) the space 10240K, 60% used [0x0000000005b80000, 0x0000000006180048, 0x0000000006180200, 0x0000000006580000) compacting perm gen total 21248K, used 2982K [0x0000000006580000, 0x0000000007a40000, 0x000000000b980000) the space 21248K, 14% used [0x0000000006580000, 0x0000000006869890, 0x0000000006869a00, 0x0000000007a40000) No shared spaces configured.
Server模式下:
Heap PSYoungGen total 9216K, used 6651K [0x000000000af20000, 0x000000000b920000, 0x000000000b920000) eden space 8192K, 81% used [0x000000000af20000,0x000000000b59ef70,0x000000000b720000) from space 1024K, 0% used [0x000000000b820000,0x000000000b820000,0x000000000b920000) to space 1024K, 0% used [0x000000000b720000,0x000000000b720000,0x000000000b820000) PSOldGen total 10240K, used 4096K [0x000000000a520000, 0x000000000af20000, 0x000000000af20000) object space 10240K, 40% used [0x000000000a520000,0x000000000a920018,0x000000000af20000) PSPermGen total 21248K, used 2972K [0x0000000005120000, 0x00000000065e0000, 0x000000000a520000) object space 21248K, 13% used [0x0000000005120000,0x0000000005407388,0x00000000065e0000)
看到在Client模式下,最后分配的4M在新生代中,先分配的6M在老年代中;在Server模式下,最后分配的4M在老年代中,先分配的6M在新生代中。說明不同的垃圾收集器組合對於對象的分配是有影響的。講下兩者差別的原因:
1、Client模式下,新生代分配了6M,虛擬機在GC前有6487K,比6M也就是6144K多,多主要是因為TLAB和EdenAllocationTest這個對象占的空間,TLAB可以通過“-XX:+PrintTLAB”這個虛擬機參數來查看大小。OK,6M多了,然后來了一個4M的,Eden+一個Survivor總共就9M不夠分配了,這時候就會觸發一次Minor GC。但是觸發Minor GC也沒用,因為allocation1、allocation2、allocation3三個引用還存在,另一塊1M的Survivor也不夠放下這6M,那么這次Minor GC的效果其實是通過分配擔保機制將這6M的內容轉入老年代中。然后再來一個4M的,由於此時Minor GC之后新生代只剩下了194K了,夠分配了,所以4M順利進入新生代。
2、Server模式下,前面都一樣,但是在GC的時候有一點區別。在GC前還會進行一次判斷,如果要分配的內存>=Eden區大小的一半,那么會直接把要分配的內存放入老年代中。要分配4M,Eden區8M,剛好一半,而且老年代10M,夠分配,所以4M就直接進入老年代去了。為了驗證一下結論,我們把3個2M之后分配的4M改為3M看一下
public class EdenAllocationTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1 = new byte[2 * _1MB]; byte[] allocation2 = new byte[2 * _1MB]; byte[] allocation3 = new byte[2 * _1MB]; byte[] allocation4 = new byte[3 * _1MB]; } }
運行結果為:
[GC [PSYoungGen: 6487K->352K(9216K)] 6487K->6496K(19456K), 0.0035661 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 352K->0K(9216K)] [PSOldGen: 6144K->6338K(10240K)] 6496K->6338K(19456K) [PSPermGen: 2941K->2941K(21248K)], 0.0035258 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 3236K [0x000000000af40000, 0x000000000b940000, 0x000000000b940000) eden space 8192K, 39% used [0x000000000af40000,0x000000000b269018,0x000000000b740000) from space 1024K, 0% used [0x000000000b740000,0x000000000b740000,0x000000000b840000) to space 1024K, 0% used [0x000000000b840000,0x000000000b840000,0x000000000b940000) PSOldGen total 10240K, used 6338K [0x000000000a540000, 0x000000000af40000, 0x000000000af40000) object space 10240K, 61% used [0x000000000a540000,0x000000000ab70858,0x000000000af40000) PSPermGen total 21248K, used 2982K [0x0000000005140000, 0x0000000006600000, 0x000000000a540000) object space 21248K, 14% used [0x0000000005140000,0x0000000005429890,0x0000000006600000)
看到3M在新生代中,6M通過分配擔保機制進入老年代了。
二、大對象直接進入老年代
需要大量連續內存空間的Java對象稱為大對象,大對象的出現會導致提前觸發垃圾收集以獲取更大的連續的空間來進行大對象的分配。虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。
看下面的代碼,虛擬機參數為“-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728”,最后那個參數表示大於這個設置值的對象直接在老年代中分配,這樣做的目的是為了避免在Eden區和兩個Survivor區之間發生大量的內存復制。
public class OldTest { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation = new byte[4 * _1MB]; } }
Client模式下
Heap def new generation total 9216K, used 507K [0x0000000005140000, 0x0000000005b40000, 0x0000000005b40000) eden space 8192K, 6% used [0x0000000005140000, 0x00000000051bef28, 0x0000000005940000) from space 1024K, 0% used [0x0000000005940000, 0x0000000005940000, 0x0000000005a40000) to space 1024K, 0% used [0x0000000005a40000, 0x0000000005a40000, 0x0000000005b40000) tenured generation total 10240K, used 4096K [0x0000000005b40000, 0x0000000006540000, 0x0000000006540000) the space 10240K, 40% used [0x0000000005b40000, 0x0000000005f40018, 0x0000000005f40200, 0x0000000006540000) compacting perm gen total 21248K, used 2972K [0x0000000006540000, 0x0000000007a00000, 0x000000000b940000) the space 21248K, 13% used [0x0000000006540000, 0x00000000068272a0, 0x0000000006827400, 0x0000000007a00000) No shared spaces configured.
Server模式下
Heap PSYoungGen total 9216K, used 4603K [0x000000000afc0000, 0x000000000b9c0000, 0x000000000b9c0000) eden space 8192K, 56% used [0x000000000afc0000,0x000000000b43ef40,0x000000000b7c0000) from space 1024K, 0% used [0x000000000b8c0000,0x000000000b8c0000,0x000000000b9c0000) to space 1024K, 0% used [0x000000000b7c0000,0x000000000b7c0000,0x000000000b8c0000) PSOldGen total 10240K, used 0K [0x000000000a5c0000, 0x000000000afc0000, 0x000000000afc0000) object space 10240K, 0% used [0x000000000a5c0000,0x000000000a5c0000,0x000000000afc0000) PSPermGen total 21248K, used 2972K [0x00000000051c0000, 0x0000000006680000, 0x000000000a5c0000) object space 21248K, 13% used [0x00000000051c0000,0x00000000054a72a0,0x0000000006680000)
看到Client模式下Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接就分配在老年代中,這是因為 PretenureSizeThreshold 被設置為3MB(就是3145728,這個參數不能像-Xmx 之類的參數一樣直接寫3MB),因此超過3MB的對象都會直接在老年代進行分配。Server模式下4M還在新生代中。產生這個差別的原因是“-XX:PretenureSizeThreshold”這個參數對Serial+Serial Old垃圾收集器組合有效而對Parallel+Serial Old垃圾收集器組合無效。
三、長期存活的對象進入老年代
每個對象有一個對象年齡計數器,與前面的對象的存儲布局中的GC分代年齡對應。對象出生在Eden區、經過一次Minor GC后仍然存活,並能夠被Survivor容納,設置年齡為1,對象在Survivor區每次經過一次Minor GC,年齡就加1,當年齡達到一定程度(默認15),就晉升到老年代,虛擬機提供了-XX:MaxTenuringThreshold來進行設置。
具體代碼如下:
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreshold(); } }
運行結果:
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 790400 bytes, 790400 total : 5174K->771K(9216K), 0.0050541 secs] 5174K->4867K(19456K), 0.0051088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4867K->0K(9216K), 0.0015279 secs] 8963K->4867K(19456K), 0.0016327 secs] [Times: user=0.00 sys=0.00, 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 4867K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffac0d30, 0x00000000ffac0e00, 0x0000000100000000) Metaspace used 2562K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K
說明:發生了兩次Minor GC,第一次是在給allocation3進行分配的時候會出現一次Minor GC,此時survivor區域不能容納allocation2,但是可以容納allocation1,所以allocation1將會進入survivor區域並且年齡為1,達到了閾值,將在下一次GC時晉升到老年代,而allocation2則會通過擔保機制進入老年代。第二次發生GC是在第二次給allocation3分配空間時,這時,allocation1的年齡加1,晉升到老年代,此次GC也可以清理出原來allocation3占據的4MB空間,將allocation3分配在Eden區。所以,最后的結果是allocation1、allocation2在老年代,allocation3在Eden區。
四、動態對象年齡判斷
對象的年齡到達了MaxTenuringThreshold可以進入老年代,同時,如果在survivor區中相同年齡所有對象大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就可以直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。
具體代碼如下:
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution * */ public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testPretenureSizeThreshold2(); } }
運行結果:
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 1048576 bytes, 1048576 total : 5758K->1024K(9216K), 0.0049451 secs] 5758K->5123K(19456K), 0.0049968 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 5120K->0K(9216K), 0.0016442 secs] 9219K->5123K(19456K), 0.0016746 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 5123K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00f80, 0x00000000ffb01000, 0x0000000100000000) Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K
結果說明:發生了兩次Minor GC,第一次發生在給allocation4分配內存時,此時allocation1、allocation2將會進入survivor區,而allocation3通過擔保機制將會進入老年代。第二次發生在給allocation4分配內存時,此時,survivor區的allocation1、allocation2達到了survivor區容量的一半,將會進入老年代,此次GC可以清理出allocation4原來的4MB空間,並將allocation4分配在Eden區。最終,allocation1、allocation2、allocation3在老年代,allocation4在Eden區。
五、空間分配擔保
在發生Minor GC時,虛擬機會檢查老年代連續的空閑區域是否大於新生代所有對象的總和,若成立,則說明Minor GC是安全的,否則,虛擬機需要查看HandlePromotionFailure的值,看是否運行擔保失敗,若允許,則虛擬機繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若大於,將嘗試進行一次Minor GC;若小於或者HandlePromotionFailure設置不運行冒險,那么此時將改成一次Full GC,以上是JDK Update 24之前的策略,之后的策略改變了,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
冒險是指經過一次Minor GC后有大量對象存活,而新生代的survivor區很小,放不下這些大量存活的對象,所以需要老年代進行分配擔保,把survivor區無法容納的對象直接進入老年代。
具體的流程圖如下:
具體代碼如下:
public class AllocationTest { private static final int _1MB = 1024 * 1024; /* * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+HandlePromotionFailure * */ public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7, allocation8; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } public static void main(String[] args) { testHandlePromotion(); } }
運行結果:
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 528280 bytes, 528280 total : 7294K->515K(9216K), 0.0040766 secs] 7294K->4611K(19456K), 0.0041309 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 6818K->0K(9216K), 0.0012444 secs] 10914K->4611K(19456K), 0.0012760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000) Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K class space used 275K, capacity 386K, committed 512K, reserved 1048576K
說明:發生了兩次GC,第一次發生在給allocation4分配內存空間時,由於老年代的連續可用空間大於存活的對象總和,所以allocation2、allocation3將會進入老年代,allocation1的空間將被回收,allocation4分配在新生代;第二次發生在給allocation7分配內存空間時,此次GC將allocation4、allocation5、allocation6所占的內存全部回收。最后,allocation2、allocation3在老年代,allocation7在新生代。