Java虛擬機(jī)中的內(nèi)存分配與回收策略就是 Java的自動內(nèi)存管理焰望,其最核心的部分就是堆內(nèi)存中對象的分配與回收。所以在了解虛擬機(jī)內(nèi)存分配與回收策略之前我們有必要了解一下Java堆內(nèi)存的組成部分寨腔。
從上圖可以得知,堆內(nèi)存主要分為新生代、老年代环凿、永久代幾部分組成集嵌,其中新生代又分為一個Eden區(qū)和兩個Survivor區(qū)萝挤,其比例為8:1。JDK1.8之后根欧,用元空間(Metaspace)的區(qū)域取代了堆中的永久代區(qū)域(永久代使用的是JVM的堆內(nèi)存空間平斩,而元空間使用的是物理內(nèi)存,直接受到本機(jī)的物理內(nèi)存限制)咽块。
-
對象優(yōu)先在Eden分配
大多數(shù)情況下绘面,對象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)中沒有足夠空間進(jìn)行分配時 ,虛擬機(jī)將發(fā)起一次Minor GC揭璃。代碼測試如下:
/*虛擬機(jī)參數(shù)配置如下 : -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails */
public class MemoryAllocate {
public static void main(String[] args) {
}
}
首先解釋下 虛擬機(jī)參數(shù)配置 : -Xms20M -Xmx20M -Xmn10M 三個參數(shù)限制了Java堆大小為20MB晚凿,不可擴(kuò)展(-Xms設(shè)置堆容量的最小值,-Xmx設(shè)置堆容量的最大值)瘦馍,其中10M分配給新生代(-Xmn設(shè)置對容量新生代的大屑呋唷),剩下的10M分配給老年代情组;-XX:SurvivorRatio=8 決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例為8:1燥筷;-XX:+PrintGCDetails 打印內(nèi)存回收日志。接下來看一下運(yùn)行結(jié)果:
Heap
PSYoungGen total 9216K, used 2148K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff819270,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3143K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 342K, capacity 388K, committed 512K, reserved 1048576K
從結(jié)果中分析得知新生代(PSYoungGen)總可用空間為Eden區(qū)與一個Survivor(from 與to其中)區(qū)的總和:(8192+1024)這里說的是新生代的可用空間院崇,而不是總空間肆氓,總空間大小為Eden與兩個Survivor區(qū)的和;老年代(ParOldGen) 的空間大小為10240k底瓣;Metaspace(元空間,JDK1.8之后用于取代永久代的空間)谢揪。因此虛擬機(jī)參數(shù)配置已經(jīng)生效,另外捐凭,雖然我們什么都沒做拨扶,Eden區(qū)的空間也已經(jīng)被使用26%。接下來看看第二段代碼:
public class MemoryAllocate {
public static void main(String[] args) {
//連續(xù)向堆中申請5個1M的空間
byte[] allocate1 = new byte[1 * 1024 * 1024];
byte[] allocate2 = new byte[1 * 1024 * 1024];
byte[] allocate3 = new byte[1 * 1024 * 1024];
byte[] allocate4 = new byte[1 * 1024 * 1024];
byte[] allocate5 = new byte[1 * 1024 * 1024];
}
}
輸出結(jié)果如下 :
Heap
PSYoungGen total 9216K, used 7598K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 92% used [0x00000000ff600000,0x00000000ffd6bab8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
PSYongGen的使用空間從2148k增加到7598k,而兩個Survivor區(qū)根本沒有使用茁肠,所以新增加的5450K空間全部在Eden中患民,證實(shí)了對象優(yōu)先在Eden區(qū)中分配的觀點(diǎn)。接下來看第三段代碼:
public class MemoryAllocate {
public static void main(String[] args) {
//連續(xù)向堆中申請5個1M的空間
byte[] allocate1 = new byte[1 * 1024 * 1024];
byte[] allocate2 = new byte[1 * 1024 * 1024];
byte[] allocate3 = new byte[1 * 1024 * 1024];
byte[] allocate4 = new byte[1 * 1024 * 1024];
byte[] allocate5 = new byte[1 * 1024 * 1024];
//再申請1一個1M的空間
byte[] allocate6 = new byte[1 * 1024 * 1024];
}
}
輸出結(jié)果如下:
[GC (Allocation Failure) [PSYoungGen: 7270K->968K(9216K)] 7270K->6096K(19456K), 0.0035282 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] [ParOldGen: 5128K->5873K(10240K)] 6096K->5873K(19456K), [Metaspace: 3218K->3218K(1056768K)], 0.0060934 secs] [Times: user=0.08 sys=0.02, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 1353K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 16% used [0x00000000ff600000,0x00000000ff7527c8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 5873K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 57% used [0x00000000fec00000,0x00000000ff1bc6f8,0x00000000ff600000)
Metaspace used 3232K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
其中觸發(fā)了一次GC回收和一次Full GC:
- [PSYoungGen: 7270K->968K(9216K)] 表示GC前年輕代占用內(nèi)存7270K垦梆,GC后占用內(nèi)存968K酒奶,內(nèi)>存區(qū)域總?cè)萘?M;
- 7270K->6096K(19456K) 表示GC前堆占用內(nèi)存7270K奶赔,GC后占用內(nèi)存6096K惋嚎,堆總?cè)萘?0M;
- [Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] 表示進(jìn)行了一次Full GC,后面會說到Full GC與GC的區(qū)別站刑。
接下來看結(jié)果:
從結(jié)果中我們可以得知ParOldGen老年代中使用了5873K的內(nèi)存另伍,而eden區(qū)中的內(nèi)存使用情況反而變小了。因?yàn)楫?dāng)給allocate6分配內(nèi)存的時候绞旅,eden區(qū)中的的剩余空間已經(jīng)不足分配allocate6所需的1M內(nèi)存摆尝, 因此發(fā)生GC,而GC期間發(fā)現(xiàn)已有的5個1M大小的對象無法全部放入Survivor空間,所以只好通過擔(dān)保分配機(jī)制將這五個對象提前轉(zhuǎn)移到老年代中因悲。
注:上面代碼運(yùn)行可能會產(chǎn)生不一樣的結(jié)果堕汞,那就需要讀者另行分析了。
-
大對象直接進(jìn)入老年代
所謂的大對象主要指晃琳,需要大量連續(xù)內(nèi)存空間的Java對象讯检,最典型的例子就是那種很長的字符串以及數(shù)組琐鲁。虛擬機(jī)提供了-XX:PretenureSizeThreshold參數(shù),令大于這個設(shè)置值的對象直接在老年代分配人灼。這樣做的好處是避免了在Eden區(qū)和兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法回收內(nèi)存)围段。
/*-XX:PretenureSizeThreshold=3145728 虛擬機(jī)參數(shù)配置*/
public class MemoryAllocate {
public static void main(String[] args) {
//申請一個8M的空間
byte[] allocate = new byte[8 * 1024 * 1024];
}
}
輸出結(jié)果 :
Heap
PSYoungGen total 9216K, used 2478K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 30% used [0x00000000ff600000,0x00000000ff86b970,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
Metaspace used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
很明顯的我們可以看到8M的空間全部分配到了老年代之中。這里有一點(diǎn)需要注意的PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效投放。本列中使用的Parallel Scavenge收集器奈泪,在所申請的空間大于eden區(qū)可使用的空間時,就會直接將大對象直接分配到老年代灸芳。
-
長期存活的對象將進(jìn)入老年代
如果對象在Eden區(qū)出生并且經(jīng)歷過一次Minor GC后仍然存活涝桅,并且能夠被Servivor容納,將被移動到Servivor空間中烙样,并且把對象年齡設(shè)置成為1冯遂。對象在Servivor區(qū)中每熬過一次Minor GC,年齡就增加1歲误阻,當(dāng)它的年齡增加到一定程度(默認(rèn)15歲),就將會被晉級到老年代中晴埂。虛擬機(jī)提供了-XX:MaxTenuringThreshold參數(shù)來設(shè)置這個閾值究反。
-
動態(tài)對象年齡判定
為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到了MaxTenuringThreshold才能晉級到老年代儒洛,如果在Servivor空間中相同年齡所有對象的大小總和大于Survivor空間的一半精耐,年齡大于或等于該年齡的對象就可以直接進(jìn)入到老年代,無須登到MaxTenuringThreshold中要求的年齡
-
空間分配擔(dān)保
在發(fā)生Minor GC 之前琅锻,虛擬機(jī)會檢查老年代最大可 用的連續(xù)空間是否大于新生代所有對象總空間卦停,如果這個條件成立,那么Minor GC可以確保是安全的恼蓬。如果不成立惊完,則虛擬機(jī)會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于晉級到老年代對象的平均大小处硬,如果大于小槐,將嘗試進(jìn)行一次Minor GC,盡管這次MinorGC 是有風(fēng)險的:如果小于荷辕,或者HandlePromotionFailure設(shè)置不允許冒險凿跳,那這時也要改為進(jìn)行一次Full GC
上面提到了Minor GC依然會有風(fēng)險,是因?yàn)樾律捎脧?fù)制收集算法疮方,假如大量對象在Minor GC后仍然存活(最極端情況為內(nèi)存回收后新生代中所有對象均存活)控嗜,而Survivor空間是比較小的,這時就需要老年代進(jìn)行分配擔(dān)保骡显,把Survivor無法容納的對象放到老年代疆栏。老年代要進(jìn)行空間分配擔(dān)保曾掂,前提是老年代得有足夠空間來容納這些對象,但一共有多少對象在內(nèi)存回收后存活下來是不可預(yù)知的承边,因此只好取之前每次垃圾回收后晉升到老年代的對象大小的平均值作為參考遭殉。使用這個平均值與老年代剩余空間進(jìn)行比較,來決定是否進(jìn)行Full GC來讓老年代騰出更多空間博助。
-
Minor GC 和 Full GC
- 新生代GC(Minor GC):指發(fā)生新生代的垃圾收集動作险污,因?yàn)镴ava對象大多數(shù)都具備朝生夕滅的特性,所以Minor GC非常頻繁富岳,一般回收速度也比較快蛔糯。
- 老年代GC(Major GC / Full GC): 指發(fā)生在老年代的GC,出現(xiàn)了Major GC窖式,至少會伴隨一次的Minor GC(但非絕對的蚁飒,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC的速度慢10倍以上萝喘。