1. 程式人生 > >JVM記憶體模型——虛擬機器棧詳細講解.md

JVM記憶體模型——虛擬機器棧詳細講解.md

0.JVM執行時資料模型

在這裡插入圖片描述

Java 虛擬機器的記憶體模型分為兩部分:一部分是執行緒共享的,包括 Java 堆和方法區;另一部分是執行緒私有的,包括虛擬機器棧和本地方法棧,以及程式計數器這一小部分記憶體。

1.程式計數器和本地方法棧

程式計數器和程式計數器比較簡單,放在一塊講。

1.1 程式計數器是一塊小的記憶體空間,執行緒私有的。

可以看做是當前執行緒所執行的位元組碼的行號指示器。每一個執行緒都有自己程式計數器。

如果執行緒正在執行的是一個Java方法,程式計數器的值就是正在執行的虛擬機器位元組碼指令的地址;如果執行緒正在執行的是Native方法,這個程式計數器的值為空(undefined)。此記憶體區域是虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

1.2 本地方法棧

本地方法棧與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是:

虛擬機器棧為虛擬機器執行java方法,而本地棧則為虛擬機器使用到的Native方法服務。Native方法是用C++實現的,在Java中以介面的方式存在,並以native修飾。

2.虛擬機器棧的工作原理

虛擬機器棧在Java方法被呼叫時起作用,虛擬機器棧的棧元素是棧幀。每當一個Java方法執行時,方法對應的棧幀入棧;執行完畢後,對應棧幀出棧。棧幀有4個部分組成,區域性變量表,運算元棧,動態連結和返回地址。
方法的引數和方法中定義的區域性變數以及就存放在區域性變量表中;方法內語句的運算元存放在運算元棧。Java 程式編譯之後就變成了一條條位元組碼指令。當執行到一條語句有n個運算元時,就用運算元棧頂中取出n個運算元,指令完成對應的計算,然後把對應的結果入棧(如果結果被賦值給了變數)。虛擬機器棧的進出棧順序是FILO原則,即先入後出,後入先出原則。比如A方法中呼叫B方法,那麼進出棧的過程是:A進棧,B進棧,B出棧,A出棧。

下面我們寫一個簡單的方法。通過反編譯得到位元組碼:
執行命令: javap -v xxx.class

public int hello(int i){
    int j =10;
    int k = i+j;
    long l = 110L;
    System.out.println(l);
    return k;
}
對應的位元組碼:
public int hello(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=6, args_size=2
         0: bipush 10
         2: istore_2
         3: iload_1
         4: iload_2
         5: iadd
         6: istore_3
         7: ldc2_w #2 // long 110l
        10: lstore 4
        12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
        15: lload 4
        17: invokevirtual #5 // Method java/io/PrintStream.println:(J)V
        20: iload_3
        21: ireturn

解讀下Java指令的執行過程(注意下面的“棧”,都是指“運算元棧”):

0: bipush 10 將一個byte型常量值10推送至棧頂,供下一條指令使用
2: istore_2 將棧頂int型數值(10)存入區域性變量表的第三個區域性變數
以上條指令對應語句:int j =10;
3: iload_1 從區域性變量表中獲取第二個int型區域性變數進棧,第二個區域性變數是hello方法的int引數i
4: iload_2 從區域性變量表中獲取第三個int型區域性變數進棧,即j
5: iadd 棧頂兩int型數值相加,並且結果進棧
6: istore_3 取棧頂(iadd 的結果)int型數值存入區域性變量表的第4個區域性變數,即k
7: ldc2_w 將long或double型常量值從常量池中推送至棧頂(寬索引)
10: lstore 4 將棧頂long型數值存入區域性變數5,即l
12: getstatic 獲取指定類的靜態域(java/lang/System.out:Ljava/io/PrintStream),並將其值壓入棧頂,對應語句System.out.println(l);
15: lload 4 將long型區域性變數5進棧
17: invokevirtual 呼叫例項方法 java/io/PrintStream.println:(J)V
20: iload_3 從區域性變量表中獲取第4個int型區域性變數進棧,即k
21: ireturn 當前方法返回int

這是hello方法在JVM中的執行過程。
下面問幾個問題

1.區域性變量表的第一個區域性變數是什麼?

是this,即當前物件,注意這個方法是成員方法才有this,如果是靜態方法就沒有this了
我們看下位元組碼檔案hello方法中有這樣的資訊,LocalVariableTable就是區域性變量表:

 LocalVariableTable:
        Start Length Slot Name Signature
            0 22 0 this Lcom/wy/jvm/StackDemo;
            0 22 1 i I
            3 19 2 j I
            7 15 3 k I
           12 10 4 l J

2.我們說一個方法對應一個棧幀,那麼遞迴呼叫會有一個棧幀,還是多個棧幀?

多個,一個方法執行時,便產生一個棧幀,多次執行就建立多個棧幀。我們可以驗證下,當一個方法遞迴呼叫死迴圈時,會丟擲StackOverflowError

3.動態連結的作用是什麼

支撐執行時的動態特性。舉個例子:

class A{
private IServer serv;
public void hello(){
    serv.work();
}
}

我們A有一個成員物件serv,它的型別是一個介面。那麼在執行hello方法時,它執行IServer 的work介面,那麼介面是不能執行的,serv只是一個引用,就得去找IServer的例項物件。那麼例項物件的地址就存放在動態連結過程,可以把這個場景類比於Spring的依賴注入執行過程。

4.區域性變數引用了成員物件,那麼它在區域性變量表中怎麼存的?

class A{
private B b = new B();
public void hello(){
    Object c = b;
}
}

例如上面情況,區域性變量表怎麼儲存c?
我們知道區域性變量表中,用32位空間儲存變數,包括了8中基本型別和引用型別。基本型別變數直接儲存,引用型別存的是一個指標。因為引用型別例項我們稱為物件,物件是存在堆裡面的,區域性變量表中就存一個物件的指標,指向堆。這是一個棧指向堆的例子。