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