JVM學習筆記1:Java虛擬機記憶體模型 學習JVM,Java虛擬機對理解Java程式執行過程和Java程式性能調優具有很大幫助。本系列博客旨在由淺到深學習並理解JVM。參考閱讀:《深入理解Java虛擬機 JVM高級特性和最佳實踐》。這個書寫的非常好,推薦有條件的讀者買一本來閱讀,網上也有電子版的。 ...
JVM學習筆記1:Java虛擬機記憶體模型
學習JVM,Java虛擬機對理解Java程式執行過程和Java程式性能調優具有很大幫助。本系列博客旨在由淺到深學習並理解JVM。參考閱讀:《深入理解Java虛擬機-JVM高級特性和最佳實踐》。這個書寫的非常好,推薦有條件的讀者買一本來閱讀,網上也有電子版的。本系列基於HotSpot虛擬機。
歡迎轉載,轉載請註明出處;筆者水平有限,錯誤之處歡迎指正!
一、Java虛擬機記憶體劃分
Java虛擬機記憶體區域按線程是否私有可以分為:
線程共用:方法區(含運行時常量池)、堆。
線程私有:虛擬機棧、本地方法棧、程式計數器。
下麵的圖可以幫助我們很直觀的理解JMM。
下麵圖來自programcreek,這個網站也是很有意思的網站
第二個圖來自《深入理解Java虛擬機-JVM高級特性和最佳實踐》
1.線程私有區
(1)程式計數器
線程是CPU調度的基本單位。每條線程使用一個獨立的程式計數器去記錄其正在執行的位元組碼指令地址。如果線程正在執行的是一個 Java方法,計數器記錄的是正在執行的位元組碼指令的地址;如果正在執行的是 Native 方法,則計數器的值為空。程式計數器是唯一一個沒有規定任何OutOfMemoryError
的區域。異常簡寫:OutOfMemoryError,OOM;SOF,StackOverflowError。下同。
(2)虛擬機棧
線程私有,java方法執行的模型。(創建線程)執行每個方法時會創建一個棧幀。棧幀包含:局部變數表、操作數棧、動態鏈接、方法出口。
局部變數表:基本類型(int,short,long,byte,float,double,boolean,char)和對象句柄(引用)。
異常情況:-Xss設置虛擬機棧大小,即深度(遞歸層次)當,棧深度>虛擬機最大棧深度,拋SOF;當申請棧記憶體大小不夠時,拋OOM。
(3)本地方法棧
執行Native方法,Native方法不是Java方法,由虛擬機實現,本地方法棧會拋SOF和OOM。
2.線程共用區
(1)Java堆
存放對象(實例),包含對象和數組。按GC情況分為新生代和老年代。物理記憶體可以不連續只有邏輯連續就可。垃圾收集(GC)會在之後的博客詳解。
堆相關虛擬機參數有:-Xmx:最大對容量 -Xms:最小堆容量。
異常情況:如果堆記憶體無法分配實例(對象),堆記憶體不夠時,拋OOM。
(2)方法區
線程共用,不需要物理連續記憶體,存放被JVM載入的類信息、常量、靜態變數、即時編譯的代碼。有時會也稱“永久代”。永久代已被移除,不再討論。
異常情況:方法區記憶體不足,拋OOM。
①運行時常量池
存放編譯期生成的字面常量和符號引用。拋OOM
字面常量:字元串、final常量值。
符號引用:類(介面)的全限定名稱、欄位的名稱和描述符、方法的名稱和描述符。
(3)方法區回收情況
常量池回收和對類型的卸載。
常量池回收判斷條件:沒有指向該常量(實例)的引用。
回收類型判斷條件:①類的所有所有實例都被回收;②載入該類的ClassLoader被回收;③該類的Class對象沒有任何地方被引用,無法通過反射訪問該類
二.java對象在jvm的創建和訪問定位
1.對象創建過程
(1)檢查所要new的類是否載入,沒有載入則執行類載入。類載入一般分為載入、鏈接、初始化,之後的博客我會講類載入機制。
(2)類載入完成,為對象分配記憶體。
為對象分配記憶體可以分兩種情況。
堆記憶體規整:指針碰撞,把分界指針向空閑記憶體移動一段對象記憶體大小的距離。
堆記憶體不規整:空閑列表,維護一個列表,從列表中查找足夠的記憶體保存對象(實例)。
(3)jvm將分配的記憶體初始化為零值。
(4)執行
2.對象在虛擬機(堆)的訪問定位
開發者通過操作引用(在棧上)來操作對象或實例(在堆上)。通過引用操縱對象的訪問方式有句柄訪問和直接指針訪問。
句柄訪問:堆上有句柄池,棧中的reference執行對象的句柄地址,句柄保存對象實例數據(在堆上)和類型數據(在方法區)的地址。如圖:
直接指針訪問:reference保存對象地址。如圖:
三.記憶體異常情況分析
1、java堆溢出(OOM)
不斷生成大對象,並保證不被GC回收。看下麵簡單的實例:
JVM執行參數:-verbose:gc -Xms20M -Xmx10M-XX:+PrintGCDetails -XX:SurvivorRatio=8。
/**
* VM Args:-Xms40M -Xmn10M
*/
public class OOMInHeap {
static class OOMInstance{}
public static void main(String[] args){
List<OOMInstance> list=new ArrayList<>();
while(true){
list.add(new OOMInstance());
}
}
}
測試結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3720)
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:485)
at java.base/java.util.ArrayList.add(ArrayList.java:498)
at com.zafkiel.OOMInHeap.main(OOMInHeap.java:14)
2、棧溢出(SOF/OOM)
(1).SOF
線程請求的棧深度大於虛擬機棧允許的最大深度,拋SOF。比如,沒有出口的遞歸方法調用。
測試實例:
/**
* VM Args: -Xss128K
* @author Zafkiel
*/
public class SOFInStack {
private int depth=1;
public void sof(){
depth++;
sof();
}
public static void main(String[] args) throws Throwable{
SOFInStack sof=new SOFInStack();
try{
sof.sof();
}catch (Throwable e){
System.out.println("stack lenth:"+sof.depth);
throw e;
}
}
}
測試結果:
Exception in thread "main" java.lang.StackOverflowError
stack lenth:7205
at com.zafkiel.SOFInStack.sof(SOFInStack.java:11)
(2).OOM
拓展棧時無法申請足夠的記憶體,則拋OOM。這種情況可以通過不停創建線程來測試。有興趣的讀者可以測試一下,註意Windows可能會假死,丟虛擬機測比較安全。
3.方法區和運行常量池溢出(OOM)
此區域可以通過不停生成類來填滿方法區,也可以通過動態類生成相關技術來實現。下麵用CGLIB動態代理來測試方法區溢出:
測試實例:
/**
* VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=5M
* @author Zafkiel
*/
public class OOMInMethodArea {
static class OOM{
static int[] array=new int[1024*1024];
}
public static void main(String[] args) throws Throwable{
try{
while (true){
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o,objects));
enhancer.create();
}
}catch (Throwable e){
System.out.println("異常信息:"+e+":"+e.getMessage());
}
}
}
測試結果(部分輸出):
異常信息:java.lang.OutOfMemoryError: Metaspace:Metaspace
這裡使用的是jdk11,永久代從jdk8之後被移到元數據區,所以JVM參數配置MetaspaceSize。
總結
對Java記憶體模型作下簡單總結: