前言
在開始介紹內(nèi)存分配策略之前碾褂,先啰嗦一下gc日志相關(guān)內(nèi)容兽间,要知道會(huì)讀gc日志是處理java虛擬機(jī)內(nèi)存問題的一項(xiàng)基本技能。接下來以一段gc日志為例正塌,詳細(xì)介紹下日志相關(guān)內(nèi)容:
[GC (Allocation Failure) --[PSYoungGen: 8192K->8192K(9216K)] 12288K->16392K(19456K), 0.0038111 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 8192K->2731K(9216K)] [ParOldGen: 8200K->8193K(10240K)] 16392K->10924K(19456K), [Metaspace: 3334K->3334K(1056768K)], 0.0056151 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
注:其實(shí)gc日志的格式是跟垃圾收集器有關(guān)的嘀略,不同的收集器,它們的格式可能都是不一樣的乓诽,但是JVM的設(shè)計(jì)者為了方便程序員們閱讀帜羊,將各個(gè)收集器的日志做了格式統(tǒng)一。
gc日志開頭的
[GC
和[Full GC
代表這次垃圾收集的類型鸠天,需要注意的是讼育,它并不是用來區(qū)分是新生代的gc還是老年代gc的,如果是Full GC稠集,只能說明這次gc是發(fā)生了STW(Stop-The-World)的奶段;-
接下來會(huì)看到
[PSYoungGen
,[ParOldGen
剥纷,[Metaspace
表示gc發(fā)生的區(qū)域痹籍,當(dāng)然這里的區(qū)域名與垃圾收集器是相關(guān)的:- 如果是Serial收集器,那么新生代名稱為
Default New Generation
晦鞋,gc日志顯示[DefNew
蹲缠; - 如果是ParNew收集器,新生代的名稱變成
Parallel New Gneration
鳖宾,gc日志顯示[ParNew
吼砂; - 如果是Parallel Scavenge收集器,gc日志則顯示
[PSYoungGen
- 如果是Serial收集器,那么新生代名稱為
區(qū)域名稱后緊跟著
8192K->8192K(9216K)
鼎文,它的意思是在gc前該內(nèi)存區(qū)域已使用的量 -> gc后該內(nèi)存區(qū)域的使用量(該內(nèi)存區(qū)域總?cè)萘浚T诜嚼ㄌ?hào)后面緊跟著12288K->16392K(19456K)
因俐,它表示gc heap已使用的量 -> gc后heap的使用量(heap的總?cè)萘浚?/p>-
在各個(gè)區(qū)域的gc相關(guān)內(nèi)存變化之后拇惋,會(huì)給出該內(nèi)存區(qū)域gc所用時(shí)間,有的收集器會(huì)給出具體的gc耗時(shí)時(shí)間數(shù)據(jù)抹剩,比如
[Times: user=0.02 sys=0.00, real=0.01 secs]
撑帖,可以看到,該時(shí)間數(shù)據(jù)包括3個(gè)時(shí)間:- user:用戶態(tài)消耗的cpu時(shí)間澳眷;
- sys:內(nèi)核態(tài)消耗的cpu時(shí)間胡嘿;
- real:操作從開始到結(jié)束所經(jīng)過的實(shí)際時(shí)間
注:需要注意的是,real時(shí)間包括各種非運(yùn)算的等待耗時(shí)钳踊,比如等待磁盤I/O衷敌,但是cpu時(shí)間是不包括這些耗時(shí)的勿侯。可能熟悉gc日志的同學(xué)可能會(huì)問缴罗,既然real的時(shí)間包括等待時(shí)間助琐,user和sys不包括等待時(shí)間,那為什么好多時(shí)候user或者sys的時(shí)間會(huì)超過real呢面氓?我們現(xiàn)在絕大多數(shù)服務(wù)器都是多cpu或者多核的兵钮,當(dāng)多個(gè)線程操作時(shí),user和sys會(huì)疊加這些cpu時(shí)間舌界,所以看到user或者sys的時(shí)間超過real是很正常的掘譬。
啰里吧嗦介紹完gc日志后,接下來我們就可以進(jìn)入正題呻拌,看一下JVM對(duì)象內(nèi)存的分配策略葱轩。
內(nèi)存分配策略
對(duì)象內(nèi)存的分配,簡單點(diǎn)兒說柏锄,就是在heap上分配內(nèi)存(JIT編譯可能間接在棧上分配)酿箭,對(duì)象首先會(huì)在Eden區(qū)分配,當(dāng)然趾娃,如果啟動(dòng)了本地線程分配緩存缭嫡,則優(yōu)先在線程的TLAB上分配,同時(shí)抬闷,也會(huì)有少數(shù)情況會(huì)在old區(qū)分配妇蛀,具體的分配細(xì)節(jié)跟垃圾收集器以及JVM內(nèi)存相關(guān)參數(shù)相關(guān)。接下來就根據(jù)實(shí)例具體分析一下相關(guān)分配策略笤成。
1. 對(duì)象首先在Eden區(qū)分配
在大多數(shù)情況下评架,對(duì)象都優(yōu)先在eden區(qū)分配內(nèi)存,當(dāng)eden區(qū)內(nèi)存空間不夠時(shí)炕泳,JVM會(huì)發(fā)起一次Monitor GC(對(duì)young區(qū)的gc)
在案例1中纵诞,通過JVM參數(shù)
-Xms20M -Xmx20M -Xmn10M
限制了heap的大小為20M并且不可擴(kuò)展,young區(qū)的大小為10M培遵,剩下的10M給old區(qū)浙芙,在main方法中,創(chuàng)建byte1到btye4四個(gè)對(duì)象籽腕,一共10M嗡呼,我們看一下會(huì)發(fā)生什么?從gc日志可以看出:
在分配bytes1到bytes3后皇耗,eden區(qū)沒有額外的空間南窗,再創(chuàng)建bytes4的時(shí)候,此時(shí)eden區(qū)內(nèi)存不夠,觸發(fā)Minitor GC万伤,本次gc結(jié)束后窒悔,yong區(qū)的6441k變成872k,但是由于bytes1到bytes3對(duì)象都是存活的壕翩,所以總得內(nèi)存量其實(shí)并沒有減少蛉迹;
在發(fā)生Monitor GC的過程中,由于Survivor空間只有1M放妈,不足以放下bytes1到bytes3的任何一個(gè)對(duì)象北救,此時(shí),通過分配擔(dān)保機(jī)制芜抒,會(huì)提前進(jìn)入old區(qū)珍策;
這次GC結(jié)束后,bytes4被順利分配在eden區(qū)宅倒,此時(shí)攘宙,eden區(qū)占用6M,Survivor空閑拐迁,old區(qū)被占用4M蹭劈。
2. 大對(duì)象直接進(jìn)入老年代
注:大對(duì)象定義:所謂的大對(duì)象,其實(shí)就是指需要大量的連續(xù)內(nèi)存空間的對(duì)象线召,比如長度很長的數(shù)組铺韧。
對(duì)于JVM來說,需要分配大對(duì)象是一個(gè)壞消息缓淹,如果程序中經(jīng)常出現(xiàn)大對(duì)象就容易導(dǎo)致gc的提前觸發(fā)哈打。當(dāng)然,JVM提供參數(shù)-XX:PretenureSizeThreshold
讯壶,一旦對(duì)象所需內(nèi)存大小大于該參數(shù)配置的閾值料仗,直接在old區(qū)為其分配內(nèi)存空間。當(dāng)然伏蚊,這樣做的目的一則是為了避免gc提前出發(fā)立轧,二則是為了避免在eden區(qū)和Survivor區(qū)發(fā)生大量的內(nèi)存拷貝。接下來還是以一個(gè)簡單的例子驗(yàn)證該規(guī)則躏吊。
在案例2中肺孵,通過參數(shù)
-XX:PretenureSizeThreshold=4194304
設(shè)置閾值為4M,一旦待分配對(duì)象大小超過4M颜阐,直接在old區(qū)進(jìn)行內(nèi)存分配。在main方法中吓肋,要?jiǎng)?chuàng)建一個(gè)大小為5M的byte數(shù)組凳怨,我們來看下gc日志,看看這個(gè)對(duì)象是不是直接在old區(qū)分配內(nèi)存的。從gc日志標(biāo)紅的地方可以看出肤舞,byte4確實(shí)直接被放在了old區(qū)紫新。
注:需要注意的是,
-XX:PretenureSizeThreshold
只對(duì)ParNew和Serial垃圾收集器有效李剖,如果你需要使用該參數(shù)的話芒率,可以使用ParNew + CMS。
3. 長期存活的對(duì)象將進(jìn)入老年代
JVM采用分代收集來管理內(nèi)存篙顺,為了在gc的時(shí)候能夠確認(rèn)哪些對(duì)象要放在young區(qū)偶芍,哪些對(duì)象放在old區(qū),JVM為給每一個(gè)對(duì)象都定義了一個(gè)年齡計(jì)數(shù)器德玫,如果對(duì)象在eden區(qū)被分配內(nèi)存并且經(jīng)過第一次Monitor GC后還存活匪蟀,此時(shí)該對(duì)象會(huì)被移動(dòng)到Survivor區(qū),對(duì)象的年齡被設(shè)置成為1宰僧,該對(duì)象在Survivor區(qū)中每經(jīng)過一次Monitor GC還不被回收材彪,年齡就加1,當(dāng)它的年齡增加達(dá)到一定的值時(shí)(默認(rèn)值是15)琴儿,對(duì)象就會(huì)被晉升到old區(qū)段化。當(dāng)然,JVM提供參數(shù)-XX:MaxTenuringThreshold
來設(shè)置對(duì)象晉升到old區(qū)的年齡閾值造成。
在案例3中显熏,通過
-XX:MaxTenuringThreshold=1
設(shè)置對(duì)象晉升到old區(qū)的年齡閾值為1,那么谜疤,gc結(jié)束后佃延,bytes1和bytes2均會(huì)進(jìn)入old區(qū),我們看一下gc日志看看是不是這樣夷磕。從gc日志可以看出履肃,經(jīng)過兩次Monitor GC,bytes1和bytes2均進(jìn)入old區(qū)坐桩,bytes3在eden區(qū)尺棋。
4. 動(dòng)態(tài)年齡判定
雖然JVM要求對(duì)象年齡必須要達(dá)到-XX:MaxTenuringThreshold
設(shè)置的閾值才能晉升到old區(qū),但是绵跷,為了更好的適應(yīng)不同的內(nèi)存使用情況膘螟,JVM增加了一個(gè)新的晉升到old區(qū)的條件:如果在Survivor區(qū)中相同年齡的對(duì)象所占內(nèi)存空間大于Survivor區(qū)的一半,不小于該年齡的對(duì)象可以直接進(jìn)入old區(qū)碾局,不需要達(dá)到-XX:MaxTenuringThreshold
設(shè)置的閾值荆残。
gc日志:
從gc日志可以看出,經(jīng)過兩次Monitor GC净当,由于bytes1所占用內(nèi)存空間大于Survivor區(qū)的一半内斯,bytes1和bytes2均進(jìn)入old區(qū)蕴潦。
5. 空間分配擔(dān)保
在發(fā)生Monitor GC之前,JVM會(huì)檢查old區(qū)的最大可用連續(xù)空間是否大于young區(qū)所有對(duì)象總空間俘闯,如果條件成立潭苞,那么Monitor GC一定是安全的,進(jìn)行一次Monitor GC真朗,如果不成立:
在jdk6 update 24之前JVM則會(huì)讀取參數(shù)
-XX:-HandlePromotionFailure
值判斷是否允許擔(dān)保失敗此疹,如果允許,檢查old區(qū)的最大可用連續(xù)空間是否大于晉升到old區(qū)對(duì)象的平均大小遮婶,如果大于蝗碎,再進(jìn)行一次Monitor GC,如果小于或者不允許擔(dān)保失敗蹭睡,則進(jìn)行一次Full GC衍菱;-
在jdk6 update 24之后,雖然JVM還定義參數(shù)
-XX:-HandlePromotionFailure
肩豁,但是已經(jīng)不會(huì)再使用它脊串,規(guī)則變?yōu)橹灰猳ld區(qū)最大可用連續(xù)空間大于晉升到old區(qū)的對(duì)象的平均大小,就進(jìn)行一次Monitor GC清钥,否則琼锋,進(jìn)行一次Full GC,JVM源碼也可以驗(yàn)證此規(guī)則:
看到這里大家估計(jì)還是有點(diǎn)懵祟昭,還是不理解為什么要空間分配擔(dān)保缕坎,接下來就解釋下為什么需要空間分配擔(dān)保。
為什么需要空間分配擔(dān)保篡悟?
注:空間分配擔(dān)保其實(shí)就是JVM確認(rèn)old區(qū)是否可以容納Monitor GC后晉升到old區(qū)的對(duì)象們谜叹。
由于young區(qū)的gc算法是復(fù)制收集算法,為了內(nèi)存的使用率搬葬,JVM只使用其中的一個(gè)Survivor取作為中間轉(zhuǎn)換空間荷腊,當(dāng)出現(xiàn)大量對(duì)象在Monitor GC后還存活的,此時(shí)急凰,Survivor區(qū)無法容納的對(duì)象直接進(jìn)入old區(qū)女仰,在進(jìn)入old區(qū)之前JVM一定要確認(rèn)old區(qū)是否有足夠的剩余空間可以容納這些對(duì)象。由于在回收之前并不知道有多少對(duì)象要進(jìn)入old區(qū)抡锈,JVM設(shè)計(jì)者認(rèn)為可以取每一次Monitor GC晉升到old區(qū)的對(duì)象容量的平均大小為經(jīng)驗(yàn)值疾忍,將該經(jīng)驗(yàn)值與old區(qū)剩余空間比較,來決定是否要進(jìn)行一次Full GC讓old區(qū)釋放出更多的空間床三。但是一罩,取平均值畢竟是一種動(dòng)態(tài)概率手段,如果某一次Monitor GC后存活對(duì)象陡增撇簿,遠(yuǎn)遠(yuǎn)高于平均值擒抛,此時(shí)還是會(huì)擔(dān)保失敗推汽,一旦出現(xiàn)擔(dān)保失敗,JVM會(huì)發(fā)起一次Full GC歧沪。