摘要:堆空間差不多是最大的記憶體空間,也是運作時資料區最重要的記憶體空間。堆可以處于實體上不連續的記憶體空間,但在邏輯上它應該被視為連續的。
本文分享自華為雲社群《醒酒菜:動畫圖解核心記憶體區--堆》,作者: 阿Q說代碼。
一般來說:
一個Java程式的運作對應一個程序;
一個程序對應着一個JVM執行個體(JVM的啟動由引導類加載器加載啟動),同時也對應着多個線程;
一個JVM執行個體擁有一個運作時資料區(Runtime類,為餓漢式單例類);
一個運作時資料區中的堆和方法區是多線程共享的,而本地方法棧、虛拟機棧、程式計數器是線程私有的。
堆空間差不多是最大的記憶體空間,也是運作時資料區最重要的記憶體空間。堆可以處于實體上不連續的記憶體空間,但在邏輯上它應該被視為連續的。
在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
堆一旦被建立,它的大小也就确定了,初始記憶體預設為電腦實體記憶體大小的1/64,最大記憶體預設為電腦實體記憶體的1/4,但是堆空間的大小是可以調節,接下來我們來示範一下。
JDK自帶記憶體分析的工具:在已安裝JDK的bin目錄下找到jvisualvm.exe。打開該軟體,下載下傳插件Visual GC,一定要點選檢查最新版本,否則會導緻安裝失敗。

安裝完重新開機jvisualvm
-Xms10m用于表示堆區的起始記憶體為10m,等價于-XX:InitialHeapSize;
-Xmx10m用于表示堆區的最大記憶體為10m,等價于-XX:MaxHeapSize;
其中-X是JVM的運作參數,ms是memory start
通常會将-Xms和-Xmx兩個參數配置相同的值,其目的就是為了能夠在java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,進而提高性能。
啟動程式之後去jvisualvm檢視
一旦堆區中的記憶體大小超過-Xmx所指定的最大記憶體時,将會抛出OOM(Out Of MemoryError)異常。
存儲在JVM中的java對象可以被劃分為兩類:
一類是生命周期較短的瞬時對象,這類對象的建立和消亡都非常迅速;
另一類是生命周期非常長,在某些情況下還能與JVM的生命周期保持一緻;
經研究表明70%-99%的對象屬于臨時對象,為了提高GC的性能,Hotspot虛拟機又将堆區進行了進一步劃分。
如圖所示,堆區又分為年輕代(YoungGen)和老年代(OldGen);其中年輕代又分為伊甸園區(Eden)和幸存者區(Survivor);幸存者區分為幸存者0區(Survivor0,S0)和幸存者1區(Survivor1,S1),有時也叫from區和to區。
分代完成之後,GC時主要檢測新生代Eden區。
新生區<=>新生代<=>年輕代
養老區<=>老年區<=>老年代
幾乎所有的Java對象都是在Eden區被new出來的,有的大對象在該區存不下可直接進入老年代。絕大部分的Java對象都銷毀在新生代了(IBM公司的專門研究表明,新生代80%的對象都是“朝生夕死”的)。
預設參數-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3;
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5;
該參數在開發中一般不會調整,如果生命周期長的對象偏多時可以選擇調整。
在HotSpot中,Eden空間和另外兩個Survivor空間所占的比例是8:1:1(測試的時候是6:1:1),開發人員可以通過選項-XX:SurvivorRatio調整空間比例,如-XX:SurvivorRatio=8
可以在cmd中通過jps 查詢程序号-> jinfo -flag NewRatio(SurvivorRatio) + 程序号 查詢配置資訊
-Xmn設定新生代最大記憶體大小(預設就好),如果既設定了該參數,又設定了NewRatio的值,則以該參數設定為準。
以上邊的代碼為例:設定啟動參數-XX:+PrintGCDetails;可在cmd視窗中輸入jps查詢程序号,然後通過jstat -gc 程序id指令檢視程序的記憶體使用情況。
new的對象先放伊甸園區,此區有大小限制;
當伊甸園的空間填滿時,程式繼續建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收(Minor GC,也叫YGC):将伊甸園區中的不再被其他對象所引用的對象進行銷毀,将未被銷毀的對象移動到幸存者0區并配置設定age;
然後再加載新的對象放到伊甸園區;
如果再次觸發垃圾回收,将此次未被銷毀的對象和上一次放在幸存者0區且此次也未被銷毀的對象一齊移動到幸存者一區,此時新對象的age為1,上次的對象的age加1變為2;
如果再次經曆垃圾回收,此時會重新放回幸存者0區,接着再去幸存者1區,age也随之增加;
預設當age為15時,未被回收的對象将移動到老年區。可以通過設定參數來更改預設配置:-XX:MaxTenuringThreshold=<N>;該過程稱為晉升(promotion);
在養老區,相對悠閑,當老年區記憶體不足時,再次觸發GC(Major GC),進行養老區的記憶體清理;
若養老區執行了Major GC之後發現依然無法進行對象的儲存,就會産生OOM異常。
S0,S1滿時不會觸發YGC,但是YGC會回收S0,S1的對象。
針對幸存者s0,s1區:複制之後有交換,誰空誰是to;
關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不再永久區/元空間收集。
新對象申請記憶體,如果Eden放的下,則直接存入Eden;如果存不下則進行YGC;
YGC之後如果能存下則放入Eden,如果還存不下(為超大對象),則嘗試存入Old區;
如果Old區可以存放,則存入;如果不能存入,則進行Full GC;
Full GC之後如果可以存入Old區,則存入;如果記憶體空間還不夠,則OOM;
圖右側為YGC的流程圖:當YGC之後未銷毀的對象放入幸存者區,此時如果幸存者區的空間可以裝下該對象,則存入幸存者區,否則,直接存入老年代;
當在幸存者區的對象超過門檻值時,可以晉升為老年代,未達到門檻值的依舊在幸存者區複制交換。
針對不同年齡段的對象配置設定原則如下:
優先配置設定到Eden;
大對象直接配置設定到老年代:盡量避免程式中出現過多的大對象;
長期存活的對象配置設定到老年代;
動态對象年齡判斷:如果Survivor區中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡;
代碼樣例,設定參數:-Xms600m,-Xmx600m
明明設定的600M,怎麼變成575M了呢?這是因為在堆記憶體存取資料時,新生代裡邊隻有伊甸園和幸存者1區或者是幸存者2區存儲對象,是以會少一個幸存者區的記憶體空間。
JVM進行GC時,并非每次都對新生代、老年代、方法區(永久代、元空間)這三個區域一起回收,大部分回收是指新生代。
針對HotSpot VM的實作,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分為:
新生代收集(Minor GC/Young GC):隻是新生代的垃圾收集;
老年代收集(Major GC/Old GC):隻是老年代的垃圾收集;
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集,隻有G1 GC (按照region劃分新生代和老年代的資料)會有這種行為。
目前,隻有CMS GC會有單獨收集老年代的行為;很多時候Major GC會和Full GC 混淆使用,需要具體分辨是老年代回收還是整堆回收。
整堆收集(Full GC):整個java堆和方法區的垃圾收集。
當年輕代空間不足時,就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。(每次Minor GC會清理年輕代的記憶體,Survivor是被動GC,不會主動GC)
因為Java對象大多都具備“朝生夕滅”的特性,是以Minor GC非常頻繁,一般回收速度也比較快。
Minor GC會引發STW(Stop The World),暫停其他使用者的線程,等垃圾回收結束,使用者線程才恢複運作。
指發生在老年代的GC,對象從老年代消失時,Major GC或者Full GC發生了;
出現了Major GC,經常會伴随至少一次的Minor GC(不是絕對的,在Parallel Scavenge收集器的收集政策裡就有直接進行Major GC的政策選擇過程),也就是老年代空間不足時,會先嘗試觸發Minor GC。如果之後空間還不足,則觸發Major GC;
Major GC速度一般會比Minor GC慢10倍以上,STW時間更長;
如果Major GC後,記憶體還不足,就報OOM了。
觸發Full GC執行的情況有以下五種:
調用System.gc()時,系統建議執行Full GC,但是不必然執行;
老年代空間不足;
方法區空間不足;
通過Minor GC後進入老年代的平均大小小于老年代的可用記憶體;
由Eden區,Survivor S0(from)區向S1(to)區複制時,對象大小大于To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小于該對象大小。
Full GC是開發或調優中盡量要避免的,這樣暫停時間會短一些。
點選關注,第一時間了解華為雲新鮮技術~