天天看點

深入了解JVM虛拟機讀書筆記——運作時棧幀結構

Java虛拟機以方法作為最基本的執行單元,“棧幀”(Stack Frame)則是用于支援虛拟機進行方法調用和方法執行背後的資料結構,它也是虛拟機運作時資料區中的虛拟機棧(Virtual Machine Stack)的棧元素。

每一個棧幀都包括了局部變量表、操作數棧、動态連接配接、方法傳回位址和一些額外的附加資訊。 如下圖(棧幀的概念結構)所示:

深入了解JVM虛拟機讀書筆記——運作時棧幀結構

1. 局部變量表(重要)

局部變量表(Local Variables Table)是一組變量值的存儲空間,用于存放方法參數和方法内部定義的局部變量。在Java程式被編譯為Class檔案時,就在方法的Code屬性的max_locals資料項中确定了該方法所需配置設定的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot)為最小機關,《Java虛拟機規範》中并沒有明确指出一個變量槽應占用的記憶體空間大小。一個變量槽可以存放一個32位以内的資料類型,Java中占用不超過32位存儲空間的資料類型有 boolean、byte、char、short、int、 float、reference 和 returnAddress 這8種類型。

第7種reference類型表示對一個對象執行個體的引用,《Java虛拟機規範》既沒有說明它的長度,也沒有明确指出這種引用應有怎樣的結構。

第8種returnAddress類型目前已經很少見了,它是為位元組碼指令jsr、jsr_w和ret服務的,指向了一條位元組碼指令的位址,某些很古老的Java虛拟機曾經使用這幾條指令來實作異常處理時的跳轉,但現在也已經全部改為采用異常表來代替了。

對于64位的資料類型,Java虛拟機會以高位對齊的方式為其配置設定兩個連續的變量槽空間。Java語言中明确的64位的資料類型隻有 long 和 double 兩種。

這裡把long和double資料類型分割存儲的做法與“long和double的非原子性協定”中允許把一次long和double資料類型讀寫分割為兩次32位讀寫的做法有些類似,讀者閱讀到本書關于Java記憶體模型的内容時可以進行對比。不過,由于局部變量表是建立線上程堆棧中的,屬于線程私有的資料,無論讀寫兩個連續的變量槽是否為原子操作,都不會引起資料競争和線程安全問題。

2. 操作數棧(重要)

操作數棧(Operand Stack)也常被稱為操作棧,它是一個後入先出(Last In First Out,LIFO)棧。操作數棧的每一個元素都可以是包括long和double在内的任意Java資料類型。32位資料類型所占的棧容量為1,64位資料類型所占的棧容量為2。

這裡列舉一個局部變量表和操作數棧互動的一個案例:

分析題:a++ + ++a的執行結果,案例代碼如下:

/**
 * 從位元組碼角度分析 a++ 相關題目
 */
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);// 11
        System.out.println(b);// 34
    }
}
      

上面a、b的結果是怎樣得來的呢?

分析:

iinc 指令是直接在局部變量桶位(slot)上進行運算。

iload指令是用于讀取變量

a++ 和 ++a 的差別是先執行 iload還是 先執行iinc。a++是先 iload再iinc,++a相反。

對虛拟機指令不清楚的去看一下這篇文章:JVM_07 類加載與位元組碼技術(位元組碼指令)

①bipush 10 操作是把a = 10 放入操作數棧:

深入了解JVM虛拟機讀書筆記——運作時棧幀結構

② 

istore 1

 操作,把操作數棧中的10彈出,放入到局部變量表的槽位1中:

深入了解JVM虛拟機讀書筆記——運作時棧幀結構

③ 接下來執行

a++

操作,我們上邊提前說明了,

a++

是先執行

iload

讀取,再執行

iinc

加 1

  • iload 1

    将 變量

    a=10

    ,讀取到操作數棧stack中:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • 執行

    iinc

    指令,在局部變量表上對a進行 +1 操作,這時候 a 為11:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • ④ 下面執行

    ++a

    操作,先

    iinc

    iload

  • iinc

    指令,在局部變量表上對a進行+1操作,這時候a為12:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • iload 1

     将局部變量表中

    a=12

  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • ⑤ 下面進行 

    a++ + ++a

     操作,在操作數棧中進行相加,得到結果22,這時候第1個加法完成:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • ⑥ 下面執行第二個加法

    (a++ + ++a)+ a--

    操作:
  • a--

    先執行

    iload

    指令,在執行

    inc 1,-1

    指令,如下,先将局部變量表中的12讀取到操作數棧:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • 接下來執行 

    inc 1,-1

    指令,在局部變量表中進行-1操作,此時局部變量表中的值由12減為11:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • 在操作數棧中,執行第二次加法運算,得到結果為34:
  • 深入了解JVM虛拟機讀書筆記——運作時棧幀結構
  • ⑦ 最後将操作數棧中的資料彈出到局部變量表中,指派2号槽位b=34:
深入了解JVM虛拟機讀書筆記——運作時棧幀結構

是以程式運作結果得到:a為11,b為34。

3. 動态連接配接(了解)

每個棧幀都包含一個指向運作時常量池[1]中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動态連接配接(Dynamic Linking)。

我們知道Class檔案的常量池中存有大量的符号引用,位元組碼中的方法調用指令就以常量池裡指向方法的符号引用作為參數。這些符号引用一部分會在類加載階段或者第一次使用的時候就被轉化為直接引用,這種轉化被稱為靜态解析。另外一部分将在每一次運作期間都轉化為直接引用,這部分就稱為動态連接配接。

4. 方法傳回位址(了解)

當一個方法開始執行後,隻有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法傳回的位元組碼指令,這時候可能會有傳回值傳遞給上層的方法調用者(調用目前方法的方法稱為調用者或者主調方法),方法是否有傳回值以及傳回值的類型将根據遇到何種方法傳回指令來決定,這種退出方法的方式稱為“正常調用完成”(Normal Method Invocation Completion)。

另外一種退出方式是在方法執行的過程中遇到了異常,并且這個異常沒有在方法體内得到妥善處理。無論是Java虛拟機内部産生的異常,還是代碼中使用athrow位元組碼指令産生的異常,隻要在本方法的異常表中沒有搜尋到比對的異常處理器,就會導緻方法退出,這種退出方法的方式稱為“異常調用完成(Abrupt Method Invocation Completion)”。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者提供任何傳回值的。

無論采用何種退出方式,在方法退出之後,都必須傳回到最初方法被調用時的位置,程式才能繼續執行,方法傳回時可能需要在棧幀中儲存一些資訊,用來幫助恢複它的上層主調方法的執行狀态。一般來說,方法正常退出時,主調方法的PC計數器的值就可以作為傳回位址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,傳回位址是要通過異常處理器表來确定的,棧幀中就一般不會儲存這部分資訊。

方法退出的過程實際上等同于把目前棧幀出棧,是以退出時可能執行的操作有:恢複上層方法的局部變量表和操作數棧,把傳回值(如果有的話)壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。

5. 附加資訊(了解)

《Java虛拟機規範》允許虛拟機實作增加一些規範裡沒有描述的資訊到棧幀之中,例如與調試、性能收集相關的資訊,這部分資訊完全取決于具體的虛拟機實作,這裡不再詳述。在讨論概念時,一般會把動态連接配接、方法傳回位址與其他附加資訊全部歸為一類,稱為棧幀資訊。

結語:

非常建議學習Java的小夥伴,買一本周志明老師的《深入了解Java虛拟機(第3版)》去讀一讀,部落格和視訊教程,始終不如看書來得實在呀!

後續會陸續更新,這本書的筆記記的差不多了,排版和格式需要花時間整理,文章都會同步到公衆号上,也歡迎大家通過公衆号加入我的交流qun互相讨論jvm這塊的知識内容!