JVM之深入了解堆
1. 堆的核心概念
堆針對一個JVM程序來說是唯一的,也就是一個程序隻有一個JVM,但是程序包含多個線程,他們是共享同一堆空間的。

- 一個JVM執行個體隻存在一個堆記憶體,
堆也是Java記憶體管理的核心區域。
- Java堆區在JVM啟動的時候即被建立,其空間大小也就确定了。是JVM管理的最大一塊記憶體空間
- 堆記憶體的大小是可以調節的。
-
《Java虛拟機規範》規定,堆可以處于實體上不連續的記憶體空間中,但在邏輯上它應該被視為連續的。// 如果設定最小堆記憶體和最大堆記憶體,建議設定成相同值 -Xms10m:最小堆記憶體 -Xmx10m:最大堆記憶體
- 所有的線程共享Java堆,在這裡
還可以劃分線程私有的緩沖區(Thread Local Allocation Buffer,TLAB)。
- 下圖就是使用:
Java VisualVM檢視堆空間的内容。
因為還有一些對象是在棧上配置設定的。
- 《Java虛拟機規範》中對Java堆的描述是:
所有的對象執行個體以及數組都應當在運作時配置設定在堆上。
我要說的是:“幾乎”所有的對象執行個體都在這裡配置設定記憶體。—從實際使用角度看的
。
- 組和對象可能永遠不會存儲在棧上,因為棧幀中儲存引用,這個引用指向對象或者數組在堆中的位置。
- 在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
也就是觸發了GC的時候,才會進行回收。
如果堆中對象馬上被回收,那麼使用者線程就會收到影響,因為有stop the word。
堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
2. 堆記憶體細分
現代垃圾收集器大部分都基于
分代收集理論
設計,堆空間細分為:
Java 7 及之前堆記憶體邏輯上分為三部分:
新生代 + 老年代 + 永久代
- Young Generation Space 新生代 Young/New
- 又被劃分為Eden區和Survivor區
- Tenure generation space 老年代 Old/Tenure
- Permanent Space 永久代 Perm
Java 8
及之後堆記憶體邏輯上分為三部分:
新生代 + 老年代 + 元空間
- Young Generation Space 新生代 Young/New
- 又被劃分為Eden區和Survivor區
- Tenure generation space 老年代 Old/Tenure
- Meta Space 元空間 Meta
-
堆空間内部結構,JDK1.8 之前從永久代 替換成 元空間
3. 設定堆記憶體大小與OOM
3.1 設定堆記憶體大小
- Java堆區用于存儲Java對象執行個體,那麼
,大家可以堆的大小在JVM啟動時就已經設定好了
通過選項"-Xmx"和"-Xms"來進行設定。
-
“-Xms"用于表示堆區的起始記憶體,等價于-xx:InitialHeapSize
-
“-Xmx"則用于表示堆區的最大記憶體,等價于-XX:MaxHeapSize
-
-
一旦堆區中的記憶體大小超過“-Xmx"所指定的最大記憶體時,将會抛出OutOfMemoryError異常。
- 通常會将
和-Xms
兩個參數配置相同的值,其目的是為了能夠在Java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,進而提高性能。-Xmx
- 預設情況下:
-
初始記憶體大小:實體電腦記憶體大小/64;
-
最大記憶體大小:實體電腦記憶體大小/4;
-
-
手動設定:-Xms600m -Xmx600m
-
開發中建議将初始堆記憶體和最大的堆記憶體設定成相同的值。
/**
* -Xms 用來設定堆空間(年輕代+老年代)的初始記憶體大小
* -X:是jvm運作參數
* ms:memory start
* -Xmx:用來設定堆空間(年輕代+老年代)的最大記憶體大小
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
// 傳回Java虛拟機中的堆記憶體總量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 傳回Java虛拟機試圖使用的最大堆記憶體
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
}
}
輸出結果
-Xms : 243M
-Xmx : 3607M
如何檢視堆記憶體的記憶體配置設定情況?
方法一:
jps 檢視程式的程序号
staat -gc 程序id 檢視堆記憶體的記憶體配置設定情況
方式二:
-XX:+PrintGCDetails
3.2 OutOfMemory舉例
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
我們用上面這個個OOM例子,然後設定啟動參數
-Xms600m -Xmx:600m
運作後,就出現OOM了,那麼我們可以通過 VisualVM這個工具
檢視具體是什麼參數造成的OOM。
4. 年輕代與老年代
- 存儲在JVM中的
Java對象可以被劃分為兩類:
- 一類是生命周期較短的瞬時對象,這類對象的建立和消亡都非常迅速。
- 生命周期短的,及時回收即可
- 另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一緻。
- Java堆區進一步細分的話,可以劃分為
年輕代(YoungGen)和老年代(oldGen)
- 其中
年輕代又可以劃分為Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區)
- 預設比例
Eden:From:to -> 8:1:1
新生代:老年代 - > 1 : 2
下面這參數開發中一般不會調:
- 配置新生代與老年代在堆結構的占比。
,表示新生代占1,老年代占2,新生代占整個堆的1/3
預設-XX:NewRatio=2
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
當發現在整個項目中,生命周期長的對象偏多,那麼就可以通過調整 老年代的大小,來進行調優。
- 在HotSpot中,
Eden空間和另外兩個survivor空間預設所占的比例是8:1:1
- 當然開發人員可以通過選項“
”調整這個空間比例。比如
-XX:SurvivorRatio
-xx:SurvivorRatio=8
絕大部分的Java對象的銷毀都在新生代進行了。
幾乎所有的Java對象都是在Eden區被new出來的。
(有些大的對象在Eden區無法存儲時候,将直接進入老年代)
IBM公司的專門研究表明,新生代中80%的對象都是“朝生夕死”的。
"-Xmn"設定新生代最大記憶體大小,這個參數一般使用預設值就可以了。
可以使用選項
5. 圖解對象配置設定過程
5.1 配置設定過程概述
為新對象配置設定記憶體是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮記憶體如何配置設定、在哪裡配置設定等問題,并且由于記憶體配置設定算法與記憶體回收算法密切相關,是以還需要考慮GC執行完記憶體回收後是否會在記憶體空間中産生記憶體碎片。
new的對象先放伊甸園區。此區有大小限制。
- 當伊甸園的空間填滿時,程式又需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收(
),将伊甸園區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區
MinorGC
- 然後将伊甸園中的剩餘對象移動到幸存者S0區。
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者S0區的,如果沒有回收,就會放到幸存者S1區。
- 如果再次經曆垃圾回收,此時會重新放回幸存者S0區,接着再去幸存者S1區。
啥時候能去老年代呢?可以設定次數。預設是15次。
- 可以設定參數:
進行設定。
-XX:MaxTenuringThreshold=N
- 在老年代,相對悠閑。當老年代記憶體不足時,再次觸發GC:
,進行老年代的記憶體清理。
Major GC
- 若老年代執行了Major GC之後,發現依然無法進行對象的儲存,就會産生OOM異常。
5.2 圖解過程
我們建立的對象,一般都是存放在Eden區的,當我們Eden區滿了後,就會觸發GC操作,
一般被稱為 YGC / Minor GC操作
- 當我們進行一次垃圾收集後,紅色的将會被回收,而綠色的還會被占用着,存放在S0(Survivor From)區。
同時我們給每個對象設定了一個年齡計數器,一次回收後就是1。
- 同時Eden區繼續存放對象,當Eden區再次存滿的時候,又會觸發一個MinorGC操作,此時GC将會把 Eden和Survivor From中的對象 進行一次收集,
把存活的對象放到 Survivor To區,同時讓年齡 + 1
3.
我們繼續不斷的進行對象生成 和 垃圾回收,當Survivor中的對象的年齡達到15的時候,将會觸發一次 Promotion晉升的操作,也就是将年輕代中的對象 晉升到 老年代中。
幸存區區滿了後?
- 特别注意,在Eden區滿了的時候,才會觸發MinorGC,而幸存者區滿了後,不會觸發MinorGC操作。
- 如果Survivor區滿了後,将會觸發
,也就是可能直接晉升老年代
一些特殊的規則
- 舉例:以當兵為例,正常人的晉升可能是 : 新兵 -> 班長 -> 排長 -> 連長
- 但是也有可能有些人因為做了非常大的貢獻,直接從 新兵 -> 排長
5.3 對象配置設定的特殊情況
代碼示範對象配置設定過程
我們不斷的建立大對象
/**
* 代碼示範對象建立過程
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然後設定JVM參數
-Xms600m -Xmx600m
然後cmd輸入下面指令,打開VisualVM圖形化界面
jvisualvm
然後通過執行上面代碼,通過VisualGC進行動态化檢視
點選下載下傳動态gif
(這個gif有點c大,是以這裡放不下,需要自行下載下傳。。。)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.atguigu.java1.HeapInstanceTest.<init>(HeapInstanceTest.java:10)
at com.atguigu.java1.HeapInstanceTest.main(HeapInstanceTest.java:15)
5.4 常用的調優工具
5.5 總結
- 針對幸存者s0,s1區的總結:複制之後有交換,誰空誰是to。
- 關于垃圾回收:頻繁在新生區收集,很少在老年代收集,幾乎不再永久代和元空間進行收集。
新生代采用複制算法的目的:是為了減少内碎片。