前言
在開始介紹內(nèi)存分配策略之前,先啰嗦一下gc日志相關(guān)內(nèi)容,要知道會讀gc日志是處理java虛擬機內(nèi)存問題的一項基本技能。接下來以一段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]
注:其實gc日志的格式是跟垃圾收集器有關(guān)的,不同的收集器,它們的格式可能都是不一樣的,但是JVM的設(shè)計者為了方便程序員們閱讀,將各個收集器的日志做了格式統(tǒng)一。
gc日志開頭的
[GC
和[Full GC
代表這次垃圾收集的類型,需要注意的是,它并不是用來區(qū)分是新生代的gc還是老年代gc的,如果是Full GC,只能說明這次gc是發(fā)生了STW(Stop-The-World)的;-
接下來會看到
[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诜嚼ㄌ柡竺婢o跟著12288K->16392K(19456K)
,它表示gc heap已使用的量 -> gc后heap的使用量(heap的總?cè)萘浚?/p>-
在各個區(qū)域的gc相關(guān)內(nèi)存變化之后,會給出該內(nèi)存區(qū)域gc所用時間,有的收集器會給出具體的gc耗時時間數(shù)據(jù),比如
[Times: user=0.02 sys=0.00, real=0.01 secs]
,可以看到,該時間數(shù)據(jù)包括3個時間:- user:用戶態(tài)消耗的cpu時間;
- sys:內(nèi)核態(tài)消耗的cpu時間;
- real:操作從開始到結(jié)束所經(jīng)過的實際時間
注:需要注意的是,real時間包括各種非運算的等待耗時,比如等待磁盤I/O,但是cpu時間是不包括這些耗時的。可能熟悉gc日志的同學(xué)可能會問,既然real的時間包括等待時間,user和sys不包括等待時間,那為什么好多時候user或者sys的時間會超過real呢?我們現(xiàn)在絕大多數(shù)服務(wù)器都是多cpu或者多核的,當(dāng)多個線程操作時,user和sys會疊加這些cpu時間,所以看到user或者sys的時間超過real是很正常的。
啰里吧嗦介紹完gc日志后,接下來我們就可以進(jìn)入正題,看一下JVM對象內(nèi)存的分配策略。
內(nèi)存分配策略
對象內(nèi)存的分配,簡單點兒說,就是在heap上分配內(nèi)存(JIT編譯可能間接在棧上分配),對象首先會在Eden區(qū)分配,當(dāng)然,如果啟動了本地線程分配緩存,則優(yōu)先在線程的TLAB上分配,同時,也會有少數(shù)情況會在old區(qū)分配,具體的分配細(xì)節(jié)跟垃圾收集器以及JVM內(nèi)存相關(guān)參數(shù)相關(guān)。接下來就根據(jù)實例具體分析一下相關(guān)分配策略。
1. 對象首先在Eden區(qū)分配
在大多數(shù)情況下,對象都優(yōu)先在eden區(qū)分配內(nèi)存,當(dāng)eden區(qū)內(nèi)存空間不夠時,JVM會發(fā)起一次Monitor GC(對young區(qū)的gc)
在案例1中,通過JVM參數(shù)
-Xms20M -Xmx20M -Xmn10M
限制了heap的大小為20M并且不可擴展,young區(qū)的大小為10M,剩下的10M給old區(qū),在main方法中,創(chuàng)建byte1到btye4四個對象,一共10M,我們看一下會發(fā)生什么?從gc日志可以看出:
在分配bytes1到bytes3后,eden區(qū)沒有額外的空間,再創(chuàng)建bytes4的時候,此時eden區(qū)內(nèi)存不夠,觸發(fā)Minitor GC,本次gc結(jié)束后,yong區(qū)的6441k變成872k,但是由于bytes1到bytes3對象都是存活的,所以總得內(nèi)存量其實并沒有減少;
在發(fā)生Monitor GC的過程中,由于Survivor空間只有1M,不足以放下bytes1到bytes3的任何一個對象,此時,通過分配擔(dān)保機制,會提前進(jìn)入old區(qū);
這次GC結(jié)束后,bytes4被順利分配在eden區(qū),此時,eden區(qū)占用6M,Survivor空閑,old區(qū)被占用4M。
2. 大對象直接進(jìn)入老年代
注:大對象定義:所謂的大對象,其實就是指需要大量的連續(xù)內(nèi)存空間的對象,比如長度很長的數(shù)組。
對于JVM來說,需要分配大對象是一個壞消息,如果程序中經(jīng)常出現(xiàn)大對象就容易導(dǎo)致gc的提前觸發(fā)。當(dāng)然,JVM提供參數(shù)-XX:PretenureSizeThreshold
,一旦對象所需內(nèi)存大小大于該參數(shù)配置的閾值,直接在old區(qū)為其分配內(nèi)存空間。當(dāng)然,這樣做的目的一則是為了避免gc提前出發(fā),二則是為了避免在eden區(qū)和Survivor區(qū)發(fā)生大量的內(nèi)存拷貝。接下來還是以一個簡單的例子驗證該規(guī)則。
在案例2中,通過參數(shù)
-XX:PretenureSizeThreshold=4194304
設(shè)置閾值為4M,一旦待分配對象大小超過4M,直接在old區(qū)進(jìn)行內(nèi)存分配。在main方法中,要創(chuàng)建一個大小為5M的byte數(shù)組,我們來看下gc日志,看看這個對象是不是直接在old區(qū)分配內(nèi)存的。從gc日志標(biāo)紅的地方可以看出,byte4確實直接被放在了old區(qū)。
注:需要注意的是,
-XX:PretenureSizeThreshold
只對ParNew和Serial垃圾收集器有效,如果你需要使用該參數(shù)的話,可以使用ParNew + CMS。
3. 長期存活的對象將進(jìn)入老年代
JVM采用分代收集來管理內(nèi)存,為了在gc的時候能夠確認(rèn)哪些對象要放在young區(qū),哪些對象放在old區(qū),JVM為給每一個對象都定義了一個年齡計數(shù)器,如果對象在eden區(qū)被分配內(nèi)存并且經(jīng)過第一次Monitor GC后還存活,此時該對象會被移動到Survivor區(qū),對象的年齡被設(shè)置成為1,該對象在Survivor區(qū)中每經(jīng)過一次Monitor GC還不被回收,年齡就加1,當(dāng)它的年齡增加達(dá)到一定的值時(默認(rèn)值是15),對象就會被晉升到old區(qū)。當(dāng)然,JVM提供參數(shù)-XX:MaxTenuringThreshold
來設(shè)置對象晉升到old區(qū)的年齡閾值。
在案例3中,通過
-XX:MaxTenuringThreshold=1
設(shè)置對象晉升到old區(qū)的年齡閾值為1,那么,gc結(jié)束后,bytes1和bytes2均會進(jìn)入old區(qū),我們看一下gc日志看看是不是這樣。從gc日志可以看出,經(jīng)過兩次Monitor GC,bytes1和bytes2均進(jìn)入old區(qū),bytes3在eden區(qū)。
4. 動態(tài)年齡判定
雖然JVM要求對象年齡必須要達(dá)到-XX:MaxTenuringThreshold
設(shè)置的閾值才能晉升到old區(qū),但是,為了更好的適應(yīng)不同的內(nèi)存使用情況,JVM增加了一個新的晉升到old區(qū)的條件:如果在Survivor區(qū)中相同年齡的對象所占內(nèi)存空間大于Survivor區(qū)的一半,不小于該年齡的對象可以直接進(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會檢查old區(qū)的最大可用連續(xù)空間是否大于young區(qū)所有對象總空間,如果條件成立,那么Monitor GC一定是安全的,進(jìn)行一次Monitor GC,如果不成立:
在jdk6 update 24之前JVM則會讀取參數(shù)
-XX:-HandlePromotionFailure
值判斷是否允許擔(dān)保失敗,如果允許,檢查old區(qū)的最大可用連續(xù)空間是否大于晉升到old區(qū)對象的平均大小,如果大于,再進(jìn)行一次Monitor GC,如果小于或者不允許擔(dān)保失敗,則進(jìn)行一次Full GC;-
在jdk6 update 24之后,雖然JVM還定義參數(shù)
-XX:-HandlePromotionFailure
,但是已經(jīng)不會再使用它,規(guī)則變?yōu)橹灰猳ld區(qū)最大可用連續(xù)空間大于晉升到old區(qū)的對象的平均大小,就進(jìn)行一次Monitor GC,否則,進(jìn)行一次Full GC,JVM源碼也可以驗證此規(guī)則:
JVM源碼
看到這里大家估計還是有點懵,還是不理解為什么要空間分配擔(dān)保,接下來就解釋下為什么需要空間分配擔(dān)保。
為什么需要空間分配擔(dān)保?
注:空間分配擔(dān)保其實就是JVM確認(rèn)old區(qū)是否可以容納Monitor GC后晉升到old區(qū)的對象們。
由于young區(qū)的gc算法是復(fù)制收集算法,為了內(nèi)存的使用率,JVM只使用其中的一個Survivor取作為中間轉(zhuǎn)換空間,當(dāng)出現(xiàn)大量對象在Monitor GC后還存活的,此時,Survivor區(qū)無法容納的對象直接進(jìn)入old區(qū),在進(jìn)入old區(qū)之前JVM一定要確認(rèn)old區(qū)是否有足夠的剩余空間可以容納這些對象。由于在回收之前并不知道有多少對象要進(jìn)入old區(qū),JVM設(shè)計者認(rèn)為可以取每一次Monitor GC晉升到old區(qū)的對象容量的平均大小為經(jīng)驗值,將該經(jīng)驗值與old區(qū)剩余空間比較,來決定是否要進(jìn)行一次Full GC讓old區(qū)釋放出更多的空間。但是,取平均值畢竟是一種動態(tài)概率手段,如果某一次Monitor GC后存活對象陡增,遠(yuǎn)遠(yuǎn)高于平均值,此時還是會擔(dān)保失敗,一旦出現(xiàn)擔(dān)保失敗,JVM會發(fā)起一次Full GC。