Java內存結構之虛擬機棧

虛擬機棧也被很多人稱爲Java棧。它是線程私有的,虛擬機棧描述的是Java方法執行的內存結構。
虛擬機棧的內存結構圖
每個方法被執行的時候都會創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息。每一個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。

棧的數據結構是先進後出。

棧幀: 是用來存儲數據和部分過程結果的數據結構。
棧幀的位置:  內存 -> 運行時數據區 -> 某個線程對應的虛擬機棧 -> here[在這裏]
棧幀大小確定時間: 編譯期確定,不受運行期數據影響。

通常有人將java內存區分爲棧和堆,實際上java內存比這複雜,這麼區分可能是因爲我們最關注,與對象內存分配關係最密切的是這兩個。

在《Java虛擬機規範》中,對這個內存區域規定了兩類異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機棧容量可以動態擴展[插圖],當棧擴展時無法申請到足夠的內存會拋出OutOfMemoryError異常。

2. Java虛擬機運行時棧幀結構

2.1 棧幀是什麼?

棧幀是一種數據結構,用於虛擬機進行方法的調用和執行。棧幀是虛擬機棧的棧元素,也就是入棧和出棧的一個單元。
棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接 (Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。

2.2 棧幀在什麼地方?

內存 -> 運行時數據區 -> 某個線程對應的虛擬機棧 -> 這裏就是棧幀了

2.3 棧幀的含義?

每個方法的執行和結束對應着棧幀的入棧和出棧。
入棧表示被調用,出棧表示執行完畢或者返回異常。
一個虛擬機棧對應一個線程,當前CPU調度的那個線程叫做活動線程;一個棧幀對應一個方法,活動線程的虛擬機棧裏最頂部的棧幀代表了當前正在執行的方法,而這個棧幀也被叫做“當前棧幀”。

2.4 棧幀既然是個數據結構,都有哪些數據?

  • 局部變量表
  • 操作數棧
  • 動態鏈接
  • 方法返回地址
  • 附加信息

2.5 棧幀的大小是什麼時候確定的?

編譯程序代碼的時候,就已經確定了局部變量表和操作數棧的大小,而且在方法表的Code屬性中寫好了。不會受到運行期數據的影響。

2.6 棧楨和棧楨是完全獨立的嗎?

本來棧楨作爲虛擬機棧的一個單元,應該是棧楨之間完全獨立的。

但是,虛擬機進行了一些優化:爲了避免過多的 方法間參數的複製傳遞、方法返回值的複製傳遞 等一些操作,就讓一部分數據進行棧楨間共享。


3. 局部變量表

3.1 什麼是局部變量表

是一片邏輯連續的內存空間,最小單位是Slot,用來存放方法參數和方法內部定義的局部變量。我覺得可以想成Slot數組…

JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”

手動翻譯:任何參數都從從局部變量0開始的連續局部變量中傳遞。

虛擬機沒有明確指明一個Slot的內存空間大小。但boolean,byte,char,short,int,float,reference,returnAddress類型的數據都可以用32位(bit位)空間或者更小的內存來存放。這些類型佔用一個Slot。Java中的long和double類型是64位(bit位),佔用兩個Slot。(只有double和long是jvms裏明確規定的64位數據類型)。

通過反編譯證實long是否佔用兩個Slot

源碼

public class Math {

    void compute(){
        int i = 1;
        long l = 2L;
        Object o = new Object();
    }

}

通過運行javap -v Math.class 得編譯後的字節碼

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=5, args_size=1
         0: iconst_1
         1: istore_1
         2: ldc2_w        #2                  // long 2l
         5: lstore_2
         6: new           #4                  // class java/lang/Object
         9: dup
        10: invokespecial #1                  // Method java/lang/Object."<init>":()V
        13: astore        4
        15: return
      LineNumberTable:
        line 7: 0
        line 8: 2
        line 9: 6
        line 10: 15
      LocalVariableTable://本地(局部)變量表
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lspring/cache/jvm/Math; //可以看出this在Slot的第0位。
            2      14     1     i   I //int i變量在Slot的第1位
            6      10     2     l   J //long l變量在Slot的第2位
           15       1     4     o   Ljava/lang/Object;//Object 由於long整型佔據兩個Slot位所以o變量在Slot的第4位
}
SourceFile: "Math.java"

3.2 虛擬機如何調用這個局部變量表?

局部變量表是有索引的,就像數組一樣。從0開始,到表的最大索引,也就是Slot的數量-1。
要注意的是,方法參數的個數 + 局部變量的個數 ≠ Slot的數量。因爲Slot的空間是可以複用的,當pc計數器的值已經超出了某個變量的作用域時,下一個變量不必使用新的Slot空間,可以去覆蓋前面那個空間。(部分內容在P183頁)

特別地,JVMS7:

On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)

手動翻譯:在一個實例方法的調用時,局部變量表的第0位是一個指向當前對象的引用,也就是Java裏的this。

通過反編譯證實局部變量表的第0位是否是this

源碼

public class Math {

    void compute(){
    }

}

通過運行javap -v Math.class 得編譯後的字節碼

 void compute();
   descriptor: ()V
   flags:
   Code:
     stack=0, locals=1, args_size=1
        0: return
     LineNumberTable:
       line 8: 0
     LocalVariableTable: //本地(局部)變量表
       Start  Length  Slot  Name   Signature
           0       1     0  this   Lspring/cache/jvm/Math;//可以看出this在Slot的第0位。
SourceFile: "Math.java"

4.操作數棧

Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.

翻譯:每個棧幀都包含一個被叫做操作數棧的後進先出的棧。叫操作棧,或者操作數棧。

Where it is clear by context, we will sometimes refer to the operand stack of the current frame as simply the operand stack.

翻譯:通常情況下,操作數棧指的就是當前棧楨的操作數棧。

4.1 操作數棧有什麼用?

操作數棧是對一些數值進行運算,任何在方法內定義的數值或引用都是先進操作數棧後再存進局部變量表,當局部變量表中的某些變量需要參與運算則應該加載進操作數棧進行操作。

通過反編譯閱讀字節碼理解操作數棧的作用

源碼

public class Math {

    int compute(){

        int price = 1000;
        price = price + 999;
        return price;

    }

}

通過運行javap -v Math.class 得編譯後的字節碼

  int compute();
    descriptor: ()I
    flags:
    Code:
      stack=2, locals=2, args_size=1
         0: sipush        1000 //將數值1000推送至棧頂(操作數棧)
         3: istore_1 //將棧頂int型數值存入局部變量表中Slot爲1的變量中。(也就是price)
         4: iload_1 //將局部變量表中Slot爲1的變量加載進操作數棧。(也就是price)
         5: sipush        999 //將整型數值999推送至棧頂(操作數棧)
         8: iadd //操作數棧中的兩個int整型進行相加並重新壓入操作數棧
         9: istore_1 //將棧頂int型數值存入局部變量表中Slot爲1的變量中。(也就是price)
        10: iload_1 //將局部變量表中Slot爲1的變量加載進操作數棧。(也就是price)
        11: ireturn //將操作數棧中的int整型數值返回
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lspring/cache/jvm/Math;
            4       8     1 price   I
}
SourceFile: "Math.java"

通過上面閱讀字節碼,可以看出在將數值賦值到變量中時,先進操作數棧再存入局部變量表中。在參與運算時再加載進操作數棧。方法返回的也是將操作數棧裏的數值進行返回。

The operand stack is empty when the frame that contains it is created. The Java virtual machine supplies instructions to load constants or values from local variables or fields onto the operand stack. Other Java virtual machine instructions take operands from the operand stack, operate on them, and push the result back onto the operand stack. The operand stack is also used to prepare parameters to be passed to methods and to receive method results.

翻譯+歸納:

1.棧楨剛創建時,裏面的操作數棧是空的。
2.Java虛擬機提供指令來讓操作數棧對一些數據進行入棧操作,比如可以把局部變量表裏的數據、實例的字段等數據入棧。
3.同時也有指令來支持出棧操作。
4.向其他方法傳參的參數,也存在操作數棧中。
5.其他方法返回的結果,返回時存在操作數棧中。

4.2 操作數棧本身就是一個普通的棧嗎?

其實棧就是棧,再加上數據結構所支持的一些指令和操作。
但是,這裏的棧也是有約束的。
操作數棧是區分類型的,操作數棧中嚴格區分類型,而且指令和類型也好嚴格匹配。

小結:
1.操作數棧其實本身就是一個棧數據結構,加上一些對字節碼操作的指令,對棧中的數據進行操作。
2.操作數棧中的數據是按照嚴格的類型區分的,操作不同類型的數據,也需用不同的指令進行操作。 例如iadd是將操作數棧頂兩int型數值相加並將結果壓入棧頂而ladd是將棧頂兩long型數值相加並將結果壓入棧頂。


5. 動態鏈接

5.1 什麼是動態鏈接

一個方法調用另一個方法,或者一個類使用另一個類的成員變量時,總得知道被調用者的名字吧?(你可以不認識它本身,但調用它就需要知道他的名字)。符號引用就相當於名字,這些被調用者的名字就存放在Java字節碼文件裏。

名字是知道了,但是Java真正運行起來的時候,真的能靠這個名字(符號引用)就能找到相應的類和方法嗎?
需要解析成相應的直接引用,利用直接引用來準確地找到。

舉個例如,就相當於我在0X0300H這個地址存入了一個數526,爲了方便編程,我把這個地址起了個別名叫A,以後我編程的時候(運行之前)可以用別名A來暗示訪問這個空間的數據,但其實程序運行起來後,實質上還是去尋找0X0300H這片空間來獲取526這個數據的。

這樣的符號引用和直接引用在運行時進行解析和鏈接的過程,叫動態鏈接。

通過反編譯閱讀字節碼理解動態鏈接

源碼

package spring.cache.jvm;

public class Math {

    void compute(){
        test();
    }

    void test(){
        String methodName = "test";
    }
}

通過運行javap -v Math.class 得編譯後的字節碼

public class spring.cache.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //常量池
// 符號引用 = 引用類型	符號引用.符號引用
   #1 = Methodref          #5.#19         // java/lang/Object."<init>":()V
   #2 = Methodref          #4.#20         // spring/cache/jvm/Math.test:()V
   #3 = String             #14            // test
   #4 = Class              #21            // spring/cache/jvm/Math
   #5 = Class              #22            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lspring/cache/jvm/Math;
  #13 = Utf8               compute
  #14 = Utf8               test
  #15 = Utf8               methodName
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               SourceFile
  #18 = Utf8               Math.java
  #19 = NameAndType        #6:#7          // "<init>":()V
  #20 = NameAndType        #14:#7         // test:()V
  #21 = Utf8               spring/cache/jvm/Math
  #22 = Utf8               java/lang/Object
{
  public spring.cache.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lspring/cache/jvm/Math;

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0 //加載局部變量表Slot爲0的變量 也就是 this
         1: invokevirtual #2                  // Method test:()V  //調用常量池符號引用爲#2的方法。
         4: return //無返回值 所以直接返回
      LineNumberTable:
        line 7: 0
        line 10: 4
      LocalVariableTable: //局部變量表
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lspring/cache/jvm/Math;

  void test();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #3                  // String test //將常量池符號引用爲#3的常量值中推送至操作數棧中
         2: astore_1 //將操作數棧中的引用類型存入局部變量表Slot位爲1的變量中,也就是methodName。(字符串是引用類型)
         3: return //無返回值 所以直接返回
      LineNumberTable:
        line 14: 0
        line 16: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature //局部變量表
            0       4     0  this   Lspring/cache/jvm/Math;
            3       1     1 methodName   Ljava/lang/String;
}
SourceFile: "Math.java"

通過閱讀字節碼我們將可以看到compute方法調用test的過程。
步驟:
1.invokevirtual #2 通過invokevirtual指令調用常量池符號引用爲#2的方法。
2.通過查看常量池發現#2符號引用對應的是 #2 = Methodref #4.#20 // spring/cache/jvm/Math.test:()V。 通過查看#2對應的類型是一個Methodref(方法引用) 後續跟#4.#20
3.通過查看#4的符號引用我們看到#4 = Class #21 // spring/cache/jvm/Math,我們得到Math的類元信息。後續跟着#21.。
4.通過查看#21的符號引用我們看到#21 = Utf8 spring/cache/jvm/Math 是一個UTF-8的字符串,該字符串是Math的全限定名。
5.回到第2步,我們定位到了#4得到了Math對象的信息,那麼#4調用的#20是什麼呢?
6.通過查看#20的符號引用我們看到#20 = NameAndType #14:#7 // test:()V#20是一個NameAndType後續跟着#14:#17
7.通過查看#14的符號引用我們看到#14 = Utf8 test 是一個UTF-8的字符串該字符串正是我們要調用的方法的方法名。
8.回到第6步再看#14冒號後面的#7是什麼呢?
9.通過查看#7的符號引用我們看到#7 = Utf8 ()V是一個UTF-8的字符串該字符串正是我們要調用的方法類型()代表這是一個方法V代表該方法的返回值void
10.回到第6步看#20 = NameAndType #14:#7 // test:()V#14:#7組合起來就是 test:()V
11.回到第2步#4.#20 其實就是 spring/cache/jvm/Math.test:()V。從而invokevirtual #2調用的就是spring/cache/jvm/Math類的test:()V方法。

5.1 動態鏈接的前提

每一個棧幀內部都有包含一個指向運行時常量池的引用,來支持動態鏈接的實現。


6. 方法返回地址

6.1 方法正常調用完成

返回一個值給調用它的方法,方法正常完成發生在一個方法執行過程中遇到了方法返回的字節碼指令的時候,使用哪種返回指令取決於方法返回值的數據類型(如果有返回值的話)。

JVMS7中的2.6.4 Normal Method Invocation Completion中寫道:

This occurs when the invoked method executes one of the return instructions (§2.11.8), the choice of which must be appropriate for the type of the value being returned (if any).

手動翻譯+理解:Java虛擬機根據不同數據類型有不同的底層return指令。當被調用方法執行某條return指令時,會選擇相應的return指令來讓值返回(如果該方法有返回值的話)。

The current frame (§2.6) is used in this case to restore the state of the invoker, including its local variables and operand stack, with the program counter of the invoker appropriately incremented to skip past the method invocation instruction. Execution then continues normally in the invoking method’s frame with the returned value (if any) pushed onto the operand stack of that frame.

手動翻譯:在這種情況,當前棧幀就被用來恢復調用者的狀態,都恢復哪些呢? 恢復局部變量表,操作數棧和程序計數器(pc指針),而這個程序技術器要適當地增加,來指向下一條指令(也就是調用函數的下一句)。使調用者方法能夠正常地繼續執行下去,而且返回值push(推送)到了調用方法的操作數棧中。

通過反編譯閱讀字節碼理解方法返回地址

源碼

package spring.cache.jvm;

public class Math {

    void compute(){
        int number = test();
    }

    int test(){
        return 1000;
    }
}

通過運行javap -v Math.class 得編譯後的字節碼

  void compute();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=2, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method test:()I	//調用this.test()方法 test()方法返回一個int類型的數值並存入當前操作數棧中
         4: istore_1 //將操作數棧中的數值賦值到Slot爲1的int變量中也就是number
         5: return
      LineNumberTable:
        line 6: 0
        line 7: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lspring/cache/jvm/Math;
            5       1     1 number   I

  int test();
    descriptor: ()I
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: sipush        1000 //將一個短整型常量值 1000 推送至棧頂
         3: ireturn	//返回操作數棧中的int整型的數值
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature //局部變量表
            0       4     0  this   Lspring/cache/jvm/Math;
}
SourceFile: "Math.java"

6.2 方法異常調用完成

異常時不會返回值給調用者。

完。

參考博客

https://www.cnblogs.com/noKing/p/8167700.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章