Java虛擬機器記憶體模型學習筆記

huangziyimark發表於2019-03-01

執行時資料區(Runtime Data Areas)

執行時資料區

一、執行緒私有區域

1、程式計數器(Program Counter Register):

執行緒私有,生命週期與執行緒同生滅,當前執行緒執行的位元組碼行號指示器。位元組碼直譯器工作時改變該記憶體值以選擇下一條需要執行的位元組碼指令。若執行緒執行的是Java方法,則程式計數器記錄的是正在執行的虛擬機器位元組碼指令地址;若執行緒執行Native方法,則計數器值為空(undefined)。該記憶體區域通常不丟擲OOM異常。

Native關鍵字作用:JNI(Java Native Interface),呼叫本機方法即非Java程式本身定義的方法,通常由C/C++編寫,至當前方法在程式外部定義。Java程式本身無需此關鍵字。

public static native void sayHello(); //sayHello()為程式外部方法,可為C/C++編譯後的檔案
2、本地方法棧(Native Method Stack):

服務虛擬機器使用到的Native方法,丟擲StackOverflowError和OutOfMemoryError異常。

3、虛擬機器棧(VM Stack):

執行緒私有,生命週期與執行緒同生滅,可丟擲StackOverflowError和OOM,是Java方法執行的記憶體模型。每個Java方法執行過程中均會建立棧幀(Stack Frame)用於儲存區域性變數區、運算元棧、動態連結、方法返回地址(returnArdess)等。方法呼叫至結束對應一個棧幀從入棧到出棧過程(遵循後進先出機制:後入棧的棧幀會優先出棧)。

通常說“堆”或“棧”記憶體模型並不準確,其記憶體結構遠比字面意義複雜。這裡的“棧”及虛擬機器棧,亦或更多指的是VM Stack中的區域性變數區。

在這裡插入圖片描述
區域性變數表:編譯期間(類載入和準備階段)完成記憶體分配,存放8種基本資料型別(int、byte、chart、short、boolean、float、long、double)、物件引用(reference)和方法返回地址(returnAdress)型別,入棧時記憶體大小為固定且已知,方法執行時不會改變大小。

64位長度的long和double型別佔用2個區域性變數空間,其餘資料型別佔用1個。

若執行緒請求的棧深度大於虛擬機器所允許的深度,則丟擲StackOverflowError異常;若虛擬機器動態擴充套件時無法申請到足夠的記憶體,則丟擲OOM異常。

/**
 * VM StackOverflowError
 * @author markyellow
 */
public class StackOverflowErrorTest {
    private int stackLength = 0;
    public void stackLeak() throws Exception {
        stackLength++;
        stackLeak(); // 增加棧深度
    }
    public static void main(String[] args) {
        // 設定VM引數測試虛擬機器棧StackOverflowError,VM options: -Xss160k(最小值)
        StackOverflowErrorTest sofe = new StackOverflowErrorTest();
        try{
            sofe.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length: " + sofe.stackLength);
            e.printStackTrace();
        }
    }
}

執行結果:

Stack Length: 771
java.lang.StackOverflowError
	at launch.StackOverflowErrorTest.stackLeak(StackOverflowErrorTest.java:13)
    ...

運算元棧:運算元棧也稱操作棧,與區域性變數區相同,其大小在編譯期確定。方法執行過程中,運算元棧棧幀用於儲存計算引數和計算結果;方法呼叫時,運算元棧用於準備呼叫方法的引數以及接收返回值。

方法剛開始執行時,運算元棧內容為空,在執行過程中,各種不同的位元組碼指令向運算元棧中寫入和讀取資料。

二、執行緒共享區域

1、Java堆(Heap):

執行緒共享的記憶體區域,在虛擬機器啟動時建立,所有的物件例項以及陣列都要在堆上分配,是垃圾回收的主要區域,也稱為GC堆(Garbage Collected Heap),大小可通過設定虛擬機器變數-Xmx和-Xms控制(不設定則表示堆大小可動態擴充套件)。根據分代收集演算法,Java堆可分為新生代(Young)老年代(Old)持久代(Permanent)
在這裡插入圖片描述
新生代(Young Generation):分為Eden區和Survivor區,其中Survivor區又可細分為From區和To區,記憶體大小比例劃分預設為Eden:From:To=8:1:1(可通過-XX:SurvivorRatio引數設定)。所有新建立的物件和陣列均存放於新生代中,若記憶體區充滿則會觸發Minor GC。

由於絕大多數新建立Object儲存於Eden Generation中,當該區域記憶體充滿並GC結束後,剩餘未被GC的物件將被移動到From區和To區。
Young Gen中的Object會被GC設定一個輪詢閾值(可通過-XX:MaxTenuringThreshold配置),當GC次數超出閾值時該物件將被推送到Old Gen中儲存。

老年代(Old Generation):存放經過多次Minor GC後仍未被GC的物件,不同於新生代的是,當老年代記憶體區域充滿後,將觸發Major GC,耗用時間較長。

老年代中的物件比較穩定,因此Major GC不會太頻繁,當Old Gen中的記憶體也被充滿後,程式將丟擲OutOfMemoryError異常。

持久代(Permanet Generation):儲存類(Class)的後設資料,用於描述類及其方法的原始資訊,可觸發Full GC。類在載入完成後即被存入Perm Gen中,主程式在執行期間不會對該記憶體區域觸發GC,當持久帶記憶體被載入的類後設資料充滿時,程式將丟擲OOM異常。

Java8中,Perm Gen已經被一個稱為“後設資料區”(元空間)的區域取代。元空間相較持久代,區別在於元空間並不在虛擬機器中,而是使用本地記憶體,從而使載入類的後設資料數量不再由-XX:MaxPermSize引數限制,而是由系統實際可用空間控制。

在這裡插入圖片描述
Java堆OOM測試程式碼:

/**
 * Java堆 OutOfMemoryError測試
 * @author markyellow
 */
public class OutOfMemoryErrorTest {
    static class OOMObject {
    }
    public static void main(String[] args) {
        /*
         * 設定VM引數測試OOM
         * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError(VM出現OOM時Dump出當前記憶體堆轉儲快照)->dump(垃圾堆)
         * 堆記憶體溢位,dump提示位置為:Java heap space
         */
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

執行結果為:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32434.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
    ...
Heap dump file created [27906673 bytes in 0.119 secs]
2、方法區(Method Area):

執行緒共享的記憶體區域,儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器(JIT)編譯後的程式碼等資料。當方法區無法滿足記憶體分配需求時將丟擲OutOfMemoryError異常。

HotSpot VM工程人員樂於將其稱為Non-Heap(非堆),也叫永久代(Permanent Generation,本質上與堆中的持久帶不同)。

在這裡插入圖片描述
執行時常量池(RunTime Constant Pool):方法區的一部分,類在編譯期產生的各種字面量和符號引用等資料將在類載入後進入執行時常量池。該記憶體區域具有動態性,除了編譯器產生的類資料外,Java程式執行期間亦可能將新的常量放入池中(譬如String.intern()方法)。

/**
 * 執行時常量池測試
 * @author markyellow
 */
public class MethodAreaTest {
    public static void main(String[] args) {
        /*
         * "aaa"存放於字串執行時常量池中
         * str4為物件引用,存放於堆中
         * str4.intern()將其存放在執行時常量池中
         */
        String str1 = "aaa";
        String str2 = "bbb";
        String str3 = "aaabbb";
        String str4 = str1 + str2;
        String str5 = "aaa" + "bbb";
        String str6 = "aaa" + str2;
        System.out.println(str3 == str4);
        System.out.println(str3 == str4.intern());
        System.out.println(str3 == str5);
        System.out.println(str3 == str6);
    }
}

執行結果:

false
false
true
true
false

附表:常用虛擬機器引數變數配置表(參考)
在這裡插入圖片描述

參考文獻:《深入理解Java虛擬機器第二版》——周志明
引用文章:
https://zhuanlan.zhihu.com/p/44694290
https://www.jianshu.com/p/50be08b54bee
https://lixh1986.iteye.com/blog/2351465
https://www.cnblogs.com/ITPower/p/7929010.html

相關文章