JVM 中堆的對(duì)象轉(zhuǎn)移與年齡判斷
1. 前言
上節(jié)課程我們講解了堆內(nèi)存中不同內(nèi)存空間模塊的作用、特點(diǎn)及意義,本節(jié)主要講解堆內(nèi)存中對(duì)象的轉(zhuǎn)移與年齡判斷。本節(jié)主要知識(shí)點(diǎn)如下:
- 理解并掌握對(duì)象優(yōu)先在 Eden 區(qū)分配的實(shí)驗(yàn)案例,為本節(jié)重點(diǎn)內(nèi)容之一;
- 理解并掌握對(duì)象直接在老年代分配的觸發(fā)條件,理解什么是大對(duì)象,為本節(jié)重點(diǎn)內(nèi)容之一;
- 掌握堆內(nèi)存對(duì)象轉(zhuǎn)移的完整流程圖及觸發(fā)機(jī)制,為本節(jié)核心知識(shí)點(diǎn),其它所有知識(shí)點(diǎn)都是圍繞這一知識(shí)點(diǎn)展開(kāi)的;
- 理解并掌握年齡判斷的定義,作用及默認(rèn)年齡值,為本節(jié)重點(diǎn)內(nèi)容之一。
通篇皆為重點(diǎn)內(nèi)容,其核心是圍繞堆內(nèi)存對(duì)象轉(zhuǎn)移的完整流程圖及觸發(fā)機(jī)制,本節(jié)課程的內(nèi)容會(huì)涉及到垃圾回收的相關(guān)概念,此處我們先做了解即可,后續(xù)會(huì)對(duì)垃圾回收進(jìn)行專門(mén)的講解。
2. 對(duì)象優(yōu)先在Eden 區(qū)分配
Tips:標(biāo)題中“優(yōu)先”一次需要學(xué)習(xí)者認(rèn)真品味,“優(yōu)先” 意味著首先考慮,那么在一些特殊情況下,新創(chuàng)建的對(duì)象還是有可能不在Eden區(qū)分配的。這種特殊情況我們?cè)谥v解老年代(OldGen)的時(shí)候再進(jìn)行說(shuō)明。
上節(jié)課程我們學(xué)習(xí)了,Eden 區(qū)屬于年輕代(YoungGen)。在創(chuàng)建新的對(duì)象時(shí),大多數(shù)情況下,對(duì)象先在 Eden 區(qū)中分配。當(dāng) Eden 區(qū)沒(méi)有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次 Minor GC。
那我們?nèi)绾芜M(jìn)行證明,新創(chuàng)建的對(duì)象優(yōu)先在Eden 區(qū)分配呢?為了對(duì)這個(gè)結(jié)論進(jìn)行驗(yàn)證,我們來(lái)設(shè)計(jì)如下實(shí)驗(yàn)。
實(shí)驗(yàn)設(shè)計(jì):
- 創(chuàng)建了一個(gè)類,類名稱可自定義,并在類中實(shí)現(xiàn)一個(gè) main 函數(shù),為后續(xù)測(cè)試做前提準(zhǔn)備;
- 在運(yùn)行main函數(shù)之前,通過(guò)設(shè)置 JVM 參數(shù),設(shè)置堆內(nèi)存初始大小為 20M,最大為 20M,其中年輕代大小為 10M,不需要特殊設(shè)置 Eden 區(qū)的大?。?/li>
- 除了設(shè)置堆內(nèi)存參數(shù)之外,還需要設(shè)置JVM 參數(shù)跟蹤詳細(xì)的垃圾回收日志,以便于觀察年輕代(YoungGen)的內(nèi)存使用情況;
- 設(shè)置完成后,main 函數(shù)不寫(xiě)任何代碼,運(yùn)行空的 main 函數(shù)觀察打印日志;
- 在main函數(shù)中創(chuàng)建一個(gè) 2M 大小的對(duì)象,運(yùn)行 main 函數(shù)觀察打印日志。
Tips:實(shí)驗(yàn)中會(huì)用到兩種JVM的參數(shù)配置,一種是配置堆內(nèi)存的參數(shù),另外一種是配置跟蹤垃圾回收的參數(shù)。這兩部分參數(shù)我們?cè)谥暗恼鹿?jié)都有詳細(xì)描述過(guò)。
實(shí)驗(yàn)要點(diǎn)準(zhǔn)備:
- 設(shè)置堆內(nèi)存大小為 20M,最大為 20M,其中年輕代大小為 10M,并設(shè)置垃圾跟蹤日志打印。需要通過(guò)JVM參數(shù)
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
進(jìn)行設(shè)置; - 不需要特殊設(shè)置 Eden 區(qū)的大小,那么年輕代中 Eden 區(qū)、from space 和 to space 將會(huì)以默認(rèn)的 8:1:1進(jìn)行空間分配;
- 創(chuàng)建一個(gè) 2M 大小的對(duì)象,我們可以通過(guò)語(yǔ)句
byte[] obj = new byte[2*1024*1024]
來(lái)實(shí)現(xiàn)。
空運(yùn)行main函數(shù)代碼演示:
public class DemoTest {
public static void main(String[] args) {
}
}
空運(yùn)行mian函數(shù)日志:
Heap
PSYoungGen total 9216K, used 2370K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 28% used [0x00000000ff600000,0x00000000ff850aa0,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 3439K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
結(jié)果分析:我們主要關(guān)注 PSYoungGen(年輕代)下的內(nèi)存分配。空運(yùn)行情況下,我們看到 Eden 區(qū)的大小為 8192K,已使用 28%。為什么空運(yùn)行下還會(huì)有 28% 的內(nèi)存使用呢?這 28% 的內(nèi)存使用,包括了支持main函數(shù)運(yùn)行的對(duì)象實(shí)例。
新建 2M 對(duì)象的代碼演示:
public class DemoTest {
public static void main(String[] args) {
byte[] obj = new byte[2*1024*1024];
}
}
新建 2M 對(duì)象的運(yùn)行日志:此處我們只展示年輕代的運(yùn)行日志。
PSYoungGen total 9216K, used 4418K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 53% used [0x00000000ff600000,0x00000000ffa50ac8,0x00000000ffe00000)
結(jié)果分析:我們看到,新建 2M 的對(duì)象之后,Eden 區(qū)使用的空間從之前的 28% 增長(zhǎng)到了 53%,凈增長(zhǎng) 25%。那么我們來(lái)進(jìn)行簡(jiǎn)單的計(jì)算 Eden 區(qū)的總內(nèi)存大小 8192K * 25% = 2048K = 2M。
看到這里我們應(yīng)該明白了,新創(chuàng)建的對(duì)象確實(shí)是優(yōu)先存儲(chǔ)于年輕代(YoungGen)中的Eden區(qū)的。
3. 大對(duì)象直接進(jìn)入老年代
我們?cè)谶M(jìn)行上一知識(shí)點(diǎn)講解時(shí)提到過(guò),新創(chuàng)建的對(duì)象是優(yōu)先存放入 Eden 區(qū)的,那么對(duì)于新創(chuàng)建的大對(duì)象來(lái)說(shuō),會(huì)直接進(jìn)入老年代碼。
什么是大對(duì)象:2M 的對(duì)象算大嗎?10M 的對(duì)象算大嗎?100M 的對(duì)象呢?什么是大對(duì)象,大對(duì)象的標(biāo)準(zhǔn)是什么?大對(duì)象的標(biāo)準(zhǔn)是可以由開(kāi)發(fā)者定義的,我們的 JVM 參數(shù)中,能夠通過(guò) -XX:PretenureSizeThreshold 這個(gè)參數(shù)設(shè)置大對(duì)象的標(biāo)準(zhǔn),可惜的是這個(gè)參數(shù)只對(duì) Serial 和 ParNew 兩款新生代收集器有效。
那么如果不能夠設(shè)置 -XX:PretenureSizeThreshold 參數(shù),那什么是大對(duì)象呢?Eden 區(qū)容量不夠存放的對(duì)象就是所謂的大對(duì)象。
為了驗(yàn)證“大對(duì)象直接進(jìn)入老年代”這一結(jié)論,我們依然通過(guò)實(shí)驗(yàn)進(jìn)行驗(yàn)證。
實(shí)驗(yàn)設(shè)計(jì):
- 沿用上一個(gè)實(shí)驗(yàn)的 JVM 參數(shù)設(shè)置,并在此基礎(chǔ)上增加參數(shù)設(shè)置
-XX:PretenureSizeThreshold = 3m
; - 將新建的 2M 對(duì)象修改為新建 6M對(duì)象;
- 運(yùn)行 main 函數(shù),觀察日志結(jié)果。
實(shí)驗(yàn)要點(diǎn)準(zhǔn)備:本實(shí)驗(yàn)所需的 JVM 參數(shù)為 -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
。
代碼示例:
public class DemoTest {
public static void main(String[] args) {
byte[] obj = new byte[6*1024*1024];
}
}
運(yùn)行結(jié)果:
Heap
PSYoungGen total 9216K, used 2370K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 28% used [0x00000000ff600000,0x00000000ff850aa0,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 6020K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 58% used [0x00000000fec00000,0x00000000ff1e1010,0x00000000ff600000)
Metaspace used 3439K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
結(jié)果分析:我們先來(lái)看下老年代(OldGen),total 10240K, used 6020K,說(shuō)明我們新創(chuàng)建的對(duì)象是直接進(jìn)入了老年代。然后我們來(lái)看下 Eden區(qū) 為什么不能存儲(chǔ) 6M 大小的對(duì)象,我們進(jìn)行簡(jiǎn)單的計(jì)算。
Eden 區(qū)剩余內(nèi)存空間 = 總空間 8192K * (1-28%)= 5898 K < 6M。這就是我們所說(shuō)的,大對(duì)象直接進(jìn)入老年代。
4. 對(duì)象轉(zhuǎn)移流程
上文我們學(xué)習(xí)了 Eden 區(qū)優(yōu)先存放新建的獨(dú)享,新建大對(duì)象不會(huì)經(jīng)過(guò)Eden區(qū),直接進(jìn)入老年代,那么還剩兩個(gè)區(qū)域沒(méi)有進(jìn)行講解:幸存者區(qū) from space 和 幸存者區(qū) to space。我們?cè)趯?duì)流程圖進(jìn)行講解時(shí),會(huì)對(duì)這兩塊內(nèi)存區(qū)域進(jìn)行說(shuō)明。
從上圖中可以看出,新生成的非大對(duì)象首先放到年輕代 Eden 區(qū),當(dāng) Eden 空間滿了,觸發(fā) Minor GC,存活下來(lái)的對(duì)象移動(dòng)到 Survivor0 區(qū),Survivor0 區(qū)滿后觸發(fā)執(zhí)行 Minor GC,Survivor0 區(qū)存活對(duì)象移動(dòng)到 Suvivor1 區(qū),這樣保證了一段時(shí)間內(nèi)總有一個(gè) survivor 區(qū)為空。經(jīng)過(guò)多次 Minor GC 仍然存活的對(duì)象移動(dòng)到老年代。
如果新生成的是大對(duì)象,會(huì)直接將該對(duì)象存放入老年代。
老年代存儲(chǔ)長(zhǎng)期存活的對(duì)象,GC 期間會(huì)停止所有線程等待 GC 完成,所以對(duì)響應(yīng)要求高的應(yīng)用盡量減少發(fā)生 Major GC,避免響應(yīng)超時(shí)。
5. 對(duì)象年齡判斷
對(duì)象年齡判斷的作用:JVM 通過(guò)判斷對(duì)象的具體年齡來(lái)判別是否該對(duì)象應(yīng)存入老年代,JVM通過(guò)對(duì)年齡的判斷來(lái)完成從對(duì)象從年輕代到老年代的轉(zhuǎn)移。
對(duì)象年齡(Age)計(jì)數(shù)器:HotSpot 虛擬機(jī)中多數(shù)收集器都采用了分代收集來(lái)管理堆內(nèi)存,那內(nèi)存回收時(shí)就必須能決策哪些存活對(duì)象應(yīng)當(dāng)放在新生代,哪些存活對(duì)象放在老年代中。為做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器,存儲(chǔ)在對(duì)象頭中。
年齡增加:對(duì)象通常在 Eden 區(qū)里誕生,如果經(jīng)過(guò)第一次 Minor GC 后仍然存活,并且能被Survivor容納的話,該對(duì)象會(huì)被移動(dòng)到 Survivor 空間中,并且將其對(duì)象年齡設(shè)為 1 歲。對(duì)象在Survivor區(qū)中每熬過(guò)一次 Minor GC,年齡就增加 1 歲。
年齡默認(rèn)閾值:當(dāng)它的年齡增加到一定程度(默認(rèn)為15),就會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過(guò)參數(shù) -XX:MaxTenuringThreshold 設(shè)置。
6. 小結(jié)
本節(jié)我們學(xué)習(xí)了堆內(nèi)存對(duì)象的轉(zhuǎn)移過(guò)程以及 JVM 是如何通過(guò)判斷對(duì)象年齡來(lái)決定是否將對(duì)象從年輕代轉(zhuǎn)移至老年代的。通篇皆為重點(diǎn)內(nèi)容,學(xué)習(xí)者需認(rèn)真對(duì)待,本節(jié)內(nèi)容與垃圾回收也息息相關(guān),學(xué)好本節(jié)課程,也能為后續(xù)垃圾回收部分打下良好的基礎(chǔ)。