JVM執行時資料區

qijian0503發表於2019-03-03

JVM執行時資料區域

根據 JVM 規範,JVM 記憶體共分為虛擬機器棧、堆、方法區、程式計數器、本地方法棧五個部分。

記憶體空間(Runtime Data Area)中可以按照是否執行緒共享分為兩塊,執行緒共享的是方法區(Method Area)和堆(Heap),執行緒獨享的是虛擬機器棧(VM Stack),本地方法棧(Native Method Stack)和PC暫存器(Program Counter Register)。

具體參見下圖:

JVM執行時資料區
區域 是否執行緒共享 是否會記憶體溢位
程式計數器 不會
虛擬機器棧
本地方法棧
方法區
  • 虛擬機器棧(執行緒私有)
    每個執行緒有一個私有的棧,隨著執行緒的建立而建立。棧裡面存放著一種叫做“棧幀”的東西,每個方法在執行的時候會建立一個棧幀,儲存了區域性變數表(基本資料型別和物件引用),運算元棧,動態連線,方法出口等資訊。
    每個方法從呼叫到執行完畢,對應一個棧幀在虛擬機器棧中的入棧和出棧。
    通常所說的棧,一般是指虛擬機器棧中的區域性變數表部分。區域性變數表所需的記憶體在編譯期間完成分配。
    棧的大小可以固定也可以動態擴充套件,當擴充套件到無法申請足夠的記憶體,則OutOfMemoryError。
    當棧呼叫深度大於JVM所允許的範圍,會丟擲StackOverflowError的錯誤,不過這個深度範圍不是一個恆定的值,我們通過下面這段程式可以測試一下這個結果:

    // 棧溢位測試原始碼
    package com.paddx.test.memory;
    /**
     * Created by root on 2/28/17.
     */
    public class StackErrorMock {
        private static int index = 1;
    
        public void call() {
            index++;
            call();
        }
    
        public static void main(String[] args) {
            StackErrorMock mock = new StackErrorMock();
            try {
                mock.call();
            } catch(Throwable e) {
                System.out.println("Stack deep: " + index);
                e.printStackTrace();
            }
        }
    }
    複製程式碼

    執行三次,可以看出每次棧的深度都是不一樣的,輸出結果如下:

    image.png
    image.png
    image.png

    檢視三張結果圖,可以看出每次的Stack deep值都有所不同。究其原因,就需要深入到JVM的原始碼中才能探討,這裡不作贅述。
    虛擬機器棧除了上述錯誤外,還有另一種錯誤,那就是當申請不到空間時,會丟擲OutOfMemoryError。這裡有一個小細節需要注意,catch捕獲的是Throwable,而不是Exception,這是因為StackOverflowError和OutOfMemoryError都不屬於Exception的子類。

  • 本地方法棧(執行緒私有)
    和虛擬機器棧類似,主要為虛擬機器使用到的Native方法服務。
    也會丟擲StackOverflowError和OutOfMemoryError。

  • PC暫存器(執行緒私有)
    PC暫存器,也叫程式計數器。JVM支援多個執行緒同時執行,每個執行緒都有自己的程式計數器。倘若當前執行的是JVM方法,則該暫存器中儲存當前執行指令的地址;倘若執行的是native方法,則PC暫存器為空。
    這個記憶體區域是唯一一個在虛擬機器中沒有規定任何OutOfMemoryError情況的區域。

  • 堆(執行緒共享)
    堆記憶體是JVM所有執行緒共享的部分,在虛擬機器啟動的時候就已經建立。
    和程式開發密切相關,應用系統物件都儲存在Java堆中。所有的物件和陣列都在堆上進行分配。這部分空間可通過GC進行回收。
    對分代GC來說,堆也是分代的,是GC的主要工作區間。
    當申請不到空間時,會丟擲OutOfMemoryError。下面我們簡單的模擬一個堆記憶體溢位的情況:

    package com.paddx.test.memory;
    import java.util.ArrayList;
    import java.util.List;
    /**
     * Created by root on 2/28/17.
     */
    public class HeapOomMock {
        public static void main(String[] args) {
            List<byte[]> list = new ArrayList<byte[]>();
            int i = 0;
            boolean flag = true;
            while(flag) {
                try {
                    i++;
                    list.add(new byte[1024 * 1024]); // 每次增加1M大小的陣列物件
                }catch(Throwable e) {
                    e.printStackTrace();
                    flag = false;
                    System.out.println("Count = " + i); // 記錄執行的次數
                }
            }
        }
    }
    複製程式碼

    首先配置執行時虛擬機器的啟動引數:

    image.png

    然後執行程式碼,輸出結果如下:

    image.png

    注意,這裡我們指定了堆記憶體的大小為16M,所以這個地方顯示的Count=13(這個數字不是固定的),至於為什麼會是13或其他數字,需要根據GC日誌來判斷。

  • 方法區(執行緒共享)
    方法區也是所有執行緒共享的。主要用於儲存類的資訊、常量池、方法資料、方法程式碼等。方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。
    這個區域的記憶體回收目標主要針對常量池的回收和對型別的解除安裝。
    當方法區無法滿足記憶體分配需求時,則丟擲OutOfMemoryError異常。
    在HotSpot虛擬機器中,用永久代來實現方法區,將GC分代收集擴充套件至方法區,但是這樣容易遇到記憶體溢位的問題。
    JDK1.7中,已經把放在永久代的字串常量池移到堆中。
    JDK1.8撤銷永久代,引入元空間。

附件(棧、堆、方法區互動):

image.png

參考連結:
segmentfault.com/a/119000000…
blog.csdn.net/universe_an…
www.cnblogs.com/mengchunche…

相關文章