Java技術體系中所提倡的自動內存管理最終能夠歸結爲自動化地解決了兩個問題:給對象分配內存以及回收分配給對象的內存。java
對象的內存分配,從大方向上將,就是在堆上分配(但也可能通過JIT編譯後被拆散爲標量類型並間接地在棧上分配),對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。少數狀況也可能直接分配在老年代中,分配的規則並非百分之百固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。算法
下面講解集中最廣泛的內存分配規則,並經過代碼去驗證這些規則。本節中的代碼在測試時使用Client模式虛擬機運行,沒有手工指定收集器組合,換句話說,驗證的是使用Serial/Serial Old收集器下(ParNew/Serial Old組合的規則也基本一致)的內存分配和回收策略。數組
大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間分配時,虛擬機將發起一次Minor GC。工具
虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,而且在進程退出的時候輸出當前內存各區域的分配狀況。在實際應用中,內存回收日誌通常都是打印到文件後經過日誌工具進行分析。測試
private static final int _1MB = 1024*1024; /** * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails */ 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 }
運行結果:spa
[GC [DefNew: 6487K->152K(9216K), 0.0041641 secs] 6487K->6296K(19456K), 0.0042253 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4412K [0x326e0000, 0x330e0000, 0x330e0000) eden space 8192K, 52% used [0x326e0000, 0x32b08fe0, 0x32ee0000) from space 1024K, 14% used [0x32fe0000, 0x330062b0, 0x330e0000) to space 1024K, 0% used [0x32ee0000, 0x32ee0000, 0x32fe0000) tenured generation total 10240K, used 6144K [0x330e0000, 0x33ae0000, 0x33ae0000) the space 10240K, 60% used [0x330e0000, 0x336e0030, 0x336e0200, 0x33ae0000) compacting perm gen total 12288K, used 375K [0x33ae0000, 0x346e0000, 0x37ae0000) the space 12288K, 3% used [0x33ae0000, 0x33b3dcb0, 0x33b3de00, 0x346e0000) ro space 10240K, 54% used [0x37ae0000, 0x3805d9f8, 0x3805da00, 0x384e0000) rw space 12288K, 55% used [0x384e0000, 0x38b813f8, 0x38b81400, 0x390e0000)
嘗試分配3個2MB和1個4MB的對象,在運行時經過-Xms20M、-Xmx20M和-Xmn10M這3個參數限制Java堆大小爲20M,且不可擴展,其中10MB分配給新生代,剩下的10M就分配給老年代了。-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的結果是新生代由6487K變爲152K,而總內存佔用了幾乎沒有減小(由於allocation1,2,3三個對象都是存活的虛擬機幾乎沒有找到可回收的對象)。此次GC發生的緣由是給allocation4分配所需的4MB內存時,發現Eden區已經被佔用了6MB,剩餘空間不足以分配4MB,所以發生Minor GC。GC期間虛擬機又發現已有的3個2MB對象沒法所有放入Survivor空間(Survivor只有1MB),因此只好經過分配擔保機制提早轉移到老年代。3d
此次GC結束後,4MB的allocation4對象被順利分配到Eden中。所以程序執行完的結果是Eden佔用4MB(被allocation4佔用),Survivor空閒,老年代被佔用6MB(allocation1,2,3佔用)。日誌
Minor和Full GC有什麼不同?
code
新生代GC(Minor GC):指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕死的特性,因此Minor GC很是頻繁,通常回收速度也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現Major GC,常常會伴隨至少一次的Minor GC(但並不是絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度通常會比Minor GC慢10倍以上。
2. 大對象直接進入老年代
所謂大對象,就是指須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組(byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來講就是一個壞消息(更加壞的狀況就是遇到一羣朝生夕死的短命大對象,寫程序時應該避免),常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置大對象。
虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接進入老年代中分配。這樣避免在Eden區及兩個Survivor區之間發生大量的內存拷貝。
/** * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:PretenureSizeThreshold=3145728 */ public static void testPretenureSizeThreshold(){ byte[] allocation; allocation = new byte[4*_1MB]; }
運行結果:
Heap def new generation total 9216K, used 671K [0x046a0000, 0x050a0000, 0x050a0000) eden space 8192K, 8% used [0x046a0000, 0x04747e88, 0x04ea0000) from space 1024K, 0% used [0x04ea0000, 0x04ea0000, 0x04fa0000) to space 1024K, 0% used [0x04fa0000, 0x04fa0000, 0x050a0000) tenured generation total 10240K, used 4096K [0x050a0000, 0x05aa0000, 0x05aa0000) the space 10240K, 40% used [0x050a0000, 0x054a0010, 0x054a0200, 0x05aa0000) compacting perm gen total 12288K, used 2130K [0x05aa0000, 0x066a0000, 0x09aa0000) the space 12288K, 17% used [0x05aa0000, 0x05cb49b8, 0x05cb4a00, 0x066a0000) No shared spaces configured.
咱們能夠看到Eden空間幾乎沒有被利用,,而老年代10MB空間被使用40%,也就是4MB的allocation對象被直接分配到老年代中,這是由於PretenureSizeThreshold被設置爲3MB,所以超過3MB的對象都會直接在老年代中進行分配。
虛擬機採用了分代收集的思想來管理內存,那內存回收時就必須識別哪些對象應該放在新生代,哪些對象應該放在老年代中。爲了作到這點,虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1。對象在Survivor區中沒熬過一次Minor GC,年齡就增長1,當它的年齡增長到必定程度(默認爲15)時,就會被晉升到老年代中。對象晉升到老年代的年齡閥值,能夠經過參數-XX:MaxTenuringThreshold來設置。
/** * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails * -XX:MaxTenuringThreshold=1 */ 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]; }
當-XX:MaxTenuringThreshold=1時,運行結果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 420200 bytes, 420200 total : 4859K->410K(9216K), 0.0042347 secs] 4859K->4506K(19456K), 0.0042967 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4506K->0K(9216K), 0.0008751 secs] 8602K->4506K(19456K), 0.0009284 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x04540000, 0x04f40000, 0x04f40000) eden space 8192K, 51% used [0x04540000, 0x04954830, 0x04d40000) from space 1024K, 0% used [0x04d40000, 0x04d40000, 0x04e40000) to space 1024K, 0% used [0x04e40000, 0x04e40000, 0x04f40000) tenured generation total 10240K, used 4506K [0x04f40000, 0x05940000, 0x05940000) the space 10240K, 44% used [0x04f40000, 0x053a6978, 0x053a6a00, 0x05940000) compacting perm gen total 12288K, used 2137K [0x05940000, 0x06540000, 0x09940000) the space 12288K, 17% used [0x05940000, 0x05b56580, 0x05b56600, 0x06540000) No shared spaces configured.
當-XX:MaxTenuringThreshold=15時,運行結果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 420200 bytes, 420200 total : 4859K->410K(9216K), 0.0069588 secs] 4859K->4506K(19456K), 0.0070540 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 2: 420056 bytes, 420056 total : 4506K->410K(9216K), 0.0012592 secs] 8602K->4506K(19456K), 0.0013433 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4588K [0x044e0000, 0x04ee0000, 0x04ee0000) eden space 8192K, 51% used [0x044e0000, 0x048f4830, 0x04ce0000) from space 1024K, 40% used [0x04ce0000, 0x04d468d8, 0x04de0000) to space 1024K, 0% used [0x04de0000, 0x04de0000, 0x04ee0000) tenured generation total 10240K, used 4096K [0x04ee0000, 0x058e0000, 0x058e0000) the space 10240K, 40% used [0x04ee0000, 0x052e0010, 0x052e0200, 0x058e0000) compacting perm gen total 12288K, used 2137K [0x058e0000, 0x064e0000, 0x098e0000) the space 12288K, 17% used [0x058e0000, 0x05af6580, 0x05af6600, 0x064e0000) No shared spaces configured.
此方法中allocation1對象須要256KB的內存空間,Survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後會很是乾淨地變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代Survivor空間,這時候新生代仍然有410KB的空間被佔用。
爲了能更好地適應不一樣程序的內存情況,虛擬機並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升到老年代,若是在Survivor空間中相同年齡全部對象大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
/** * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -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]; }
運行結果:
[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 682360 bytes, 682360 total : 5115K->666K(9216K), 0.0068333 secs] 5115K->4762K(19456K), 0.0069434 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4762K->0K(9216K), 0.0015284 secs] 8858K->4762K(19456K), 0.0016157 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x04470000, 0x04e70000, 0x04e70000) eden space 8192K, 51% used [0x04470000, 0x04884830, 0x04c70000) from space 1024K, 0% used [0x04c70000, 0x04c70000, 0x04d70000) to space 1024K, 0% used [0x04d70000, 0x04d70000, 0x04e70000) tenured generation total 10240K, used 4762K [0x04e70000, 0x05870000, 0x05870000) the space 10240K, 46% used [0x04e70000, 0x053168f8, 0x05316a00, 0x05870000) compacting perm gen total 12288K, used 2137K [0x05870000, 0x06470000, 0x09870000) the space 12288K, 17% used [0x05870000, 0x05a86580, 0x05a86600, 0x06470000) No shared spaces configured.
發現運行結果中Survivor佔用仍然爲0%,而老年代比預期增長了6%,也就是說allocation1,allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。由於這兩個對象加起來達到了512KB,而且它們是同年的,知足同年對象達到Survivor空間的一半規則。咱們只要註釋一個對象的new操做,就會發現另一個不會晉升到老年代了。
當發生Minor GC時,虛擬機會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC。若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗;若是容許,那隻會進行Minor GC;若是不容許,則也要改成進行一次Full GC。
新生代使用複製收集算法,但爲了內存利用率,值使用其中一個Survivor空間來做爲輪換備份,所以當出現大量對象在Minor GC後仍然存活的狀況時(最極端就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,讓Survivor沒法容納的對象直接進入老年代。與生活中的貸款擔保相似,老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下去,在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗,與老年代的剩餘空間進行對比,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態機率的手段,也就是說若是某次Minor GC存活後的對象突增,遠遠高於平均值時,依然會致使擔保失敗(Handle Promotion Failure)。若是出現HandlePromotionFailure失敗,那就只好在失敗後從新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。
/** * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 * -XX:-HandlePromotionFailure -XX:+PrintGCDetails */ public static void testHandlePromotion(){ byte[] allocation1,allocation2,allocation3,allocation4,allocation5, allocation6,allocation7; 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]; }
以HandlePromotionFailure=false運行結果:
[GC [DefNew: 6651K->154K(9216K), 0.0033102 secs] 6651K->4250K(19456K), 0.0033813 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 6384K->6384K(9216K), 0.0000288 secs][Tenured: 4096K->4250K(10240K), 0.0039019 secs] 10480K->4250K(19456K), [Perm : 2132K->2132K(12288K)], 0.0040460 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2211K [0x044e0000, 0x04ee0000, 0x04ee0000) eden space 8192K, 27% used [0x044e0000, 0x04708fe0, 0x04ce0000) from space 1024K, 0% used [0x04de0000, 0x04de0000, 0x04ee0000) to space 1024K, 0% used [0x04ce0000, 0x04ce0000, 0x04de0000) tenured generation total 10240K, used 4250K [0x04ee0000, 0x058e0000, 0x058e0000) the space 10240K, 41% used [0x04ee0000, 0x053068a8, 0x05306a00, 0x058e0000) compacting perm gen total 12288K, used 2137K [0x058e0000, 0x064e0000, 0x098e0000) the space 12288K, 17% used [0x058e0000, 0x05af6710, 0x05af6800, 0x064e0000) No shared spaces configured.
以HandlePromotionFailure=true運行結果:
[GC [DefNew: 6651K->154K(9216K), 0.0038289 secs] 6651K->4250K(19456K), 0.0038877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 6384K->154K(9216K), 0.0006008 secs] 10480K->4250K(19456K), 0.0006525 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2366K [0x043d0000, 0x04dd0000, 0x04dd0000) eden space 8192K, 27% used [0x043d0000, 0x045f8fe0, 0x04bd0000) from space 1024K, 15% used [0x04bd0000, 0x04bf6888, 0x04cd0000) to space 1024K, 0% used [0x04cd0000, 0x04cd0000, 0x04dd0000) tenured generation total 10240K, used 4096K [0x04dd0000, 0x057d0000, 0x057d0000) the space 10240K, 40% used [0x04dd0000, 0x051d0020, 0x051d0200, 0x057d0000) compacting perm gen total 12288K, used 2137K [0x057d0000, 0x063d0000, 0x097d0000) the space 12288K, 17% used [0x057d0000, 0x059e6710, 0x059e6800, 0x063d0000) No shared spaces configured.