1. 程式人生 > >Java虛擬機器-----執行時棧結構

Java虛擬機器-----執行時棧結構

Java 虛擬機器棧

      該區域也是執行緒私有的,它的生命週期也與執行緒相同。虛擬機器棧描述的是 Java 方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,棧它是用於支援續虛擬機器進行方法呼叫和方法執行的資料結構。對於執行引擎來講,活動執行緒中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所執行的所有位元組碼指令都只針對當前棧幀進行操作。棧幀用於儲存區域性變量表、運算元棧、動態連結、方法返回地址和一些額外的附加資訊。在編譯程式程式碼時,棧幀中需要多大的區域性變量表、多深的運算元棧都已經完全確定了,並且寫入了方法表的 Code 屬性之中。因此,一個棧幀需要分配多少記憶體,不會受到程式執行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。

在 Java 虛擬機器規範中,對這個區域規定了兩種異常情況:

  • 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。
  • 如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。
  • 這兩種情況存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是記憶體太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。在單執行緒的操作中,無論是由於棧幀太大,還是虛擬機器棧空間太小,當棧空間無法分配時,虛擬機器丟擲的都是 StackOverflowError 異常,而不會得到 OutOfMemoryError 異常。而在多執行緒環境下,則會丟擲 OutOfMemoryError 異常。

    棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區的虛擬機器棧(Virtual Machine Stack)的棧元素。棧幀儲存了方法的區域性變量表,運算元棧,動態連線和方法返回地址等資訊。第一個方法從呼叫開始到執行完成,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。main函式是程式的入口函式,所以它會存在於棧底。

    棧幀的概念結構如下圖所示:

    這裡寫圖片描述
    - 區域性變量表
          區域性變量表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數,其中存放的資料的型別是編譯期可知的各種基本資料型別、物件引用(reference)和 returnAddress 型別(它指向了一條位元組碼指令的地址)。區域性變量表所需的記憶體空間在編譯期間完成分配,即在 Java 程式被編譯成 Class 檔案時,就確定了所需分配的最大區域性變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。

          區域性變量表的容量以變數槽(Slot)為最小單位。在虛擬機器規範中並沒有明確指明一個 Slot 應占用的記憶體空間大小(允許其隨著處理器、作業系統或虛擬機器的不同而發生變化),一個 Slot 可以存放一個32位以內的資料型別:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是物件的引用型別,returnAddress 是為位元組指令服務的,它執行了一條位元組碼指令的地址(基本用不著了)。對於 64 位的資料型別(long和double),虛擬機器會以高位在前的方式為其分配兩個連續的 Slot 空間。

    虛擬機器通過索引定位的方式使用區域性變量表,索引值的範圍是從 0 開始到區域性變量表最大的 Slot 數量,對於 32 位資料型別的變數,索引 n 代表第 n 個 Slot,對於 64 位的,索引 n 代表第 n 和第 n+1 兩個 Slot。

    在方法執行時,虛擬機器是使用區域性變量表來完成引數值到引數變數列表的傳遞過程的,如果是例項方法(非static),則區域性變量表中的第 0 位索引的 Slot 預設是用於傳遞方法所屬物件例項的引用,在方法中可以通過關鍵字“this”來訪問這個隱含的引數。其餘引數則按照引數表的順序來排列,佔用從1開始的區域性變數 Slot,引數表分配完畢後,再根據方法體內部定義的變數順序和作用域分配其餘的 Slot。

    區域性變量表中的 Slot 是可重用的,方法體中定義的變數,作用域並不一定會覆蓋整個方法體,如果當前位元組碼PC計數器的值已經超過了某個變數的作用域,那麼這個變數對應的 Slot 就可以交給其他變數使用。這樣的設計不僅僅是為了節省空間,在某些情況下 Slot 的複用會直接影響到系統的而垃圾收集行為。

    2、運算元棧

    • 運算元棧又常被稱為操作棧,運算元棧的最大深度也是在編譯的時候就確定了。32 位資料型別所佔的棧容量為 1,64 位資料型別所佔的棧容量為 2。當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種位元組碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內容,也就是入棧和出棧操作。
    • Java 虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。因此我們也稱 Java 虛擬機器是基於棧的,這點不同於 Android 虛擬機器,Android 虛擬機器是基於暫存器的。
    • 基於棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由於暫存器由硬體直接提供,所以基於暫存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差。

    3、動態連結

    • 每個棧幀都包含一個指向執行時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。Class 檔案的常量池中存在有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用為引數。這些符號引用,一部分會在類載入階段或第一次使用的時候轉化為直接引用(如 final、static 域等),稱為靜態解析,另一部分將在每一次的執行期間轉化為直接引用,這部分稱為動態連線。

    4、方法返回地址

    • 當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的位元組碼指令或遇到了異常,並且該異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行。方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者的 PC 計數器的值就可以作為返回地址,棧幀中很可能儲存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。
    • 方法退出的過程實際上等同於把當前棧幀出站,因此退出時可能執行的操作有:恢復上層方法的區域性變量表和運算元棧,如果有返回值,則把它壓入呼叫者棧幀的運算元棧中,調整 PC 計數器的值以指向方法呼叫指令後面的一條指令。

    最後看一個小小的栗子理解一下概念

    import java.lang.*;
    class Test{
        public int calc(){
            int a = 100;
            int b = 200;
            int c = 300;
            return (a*b)*c;
        }
    }

    javac Test.java 編譯該程式
    javap -verbose Test 檢視java編譯後class檔案的位元組碼(反彙編)

    結果看下面:

    Classfile /home/panlu/Programs/java實驗/Test.class
      Last modified 2015-11-19; size 262 bytes
      MD5 checksum 9e8f73723448cc61033ada1236b12d3e
      Compiled from "Test.java"
    class Test
      minor version: 0
      major version: 52
      flags: ACC_SUPER
    Constant pool:
       #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
       #2 = Class              #13            // Test
       #3 = Class              #14            // java/lang/Object
       #4 = Utf8               <init>
       #5 = Utf8               ()V
       #6 = Utf8               Code
       #7 = Utf8               LineNumberTable
       #8 = Utf8               calc
       #9 = Utf8               ()I
      #10 = Utf8               SourceFile
      #11 = Utf8               Test.java
      #12 = NameAndType        #4:#5          // "<init>":()V
      #13 = Utf8               Test
      #14 = Utf8               java/lang/Object
    {
      Test();
        descriptor: ()V
        flags:
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 9: 0
    
      public int calc();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: bipush        100
             2: istore_1
             3: sipush        200
             6: istore_2
             7: sipush        300
            10: istore_3
            11: iload_1
            12: iload_2
            13: imul
            14: iload_3
            15: imul
            16: ireturn
          LineNumberTable:
            line 11: 0
            line 12: 3
            line 13: 7
            line 14: 11
    }
    SourceFile: "Test.java"

    這裡寫圖片描述

    解釋一下這個圖片:
    程式計數器從0號開始指示,每執行一句往下計數,為什麼從0直接跳到2了呢?偏移量增加2是因為bipush指令佔用一個偏移量 引數100也佔用了一個偏移量。

    0: bipush       100   //將100壓棧
    2: istore_1           //將100出棧放那放入區域性變數的1號Slot內
    //200 300如上
    11: iload_1      //將區域性變數區的第1個變數壓入棧
    12: iload_2      //將區域性變數區的第2個變數壓入棧
    13: imul         //將棧中的兩個資料取出來進行相乘再放入操作棧中
    14: iload_3
    15: imul
    16: ireturn     //返回