Java虛擬機—棧幀、操作數棧和局部變量表

9.jpg

前言:

在之前的文章:Java虛擬機—堆、棧、運行時數據區 中,我們整體介紹了JVM在運行時的一些數據區域如堆、方法區、程序計數器、虛擬機棧、本地方法棧。本篇文章,我們圍繞其中的一個區域展開——虛擬機棧中的棧元素棧幀

所以,本文的主要分為兩部分:

1.Java虛擬機運行時棧幀介紹 2.一個關于字節碼指令以及操作數出棧/入棧過程的小實例

其中,運行時棧幀介紹主要包括:

  • 0.棧幀的概念
  • 1.局部變量表
  • 2.操作數棧
  • 3.動態鏈接
  • 4.方法返回
  • 5.附加信息

Java虛擬機棧和運行時棧幀結構

Java虛擬機是基于「棧」架構的,如圖所示:

image

為什么要深入研究虛擬機棧呢?因為它hin重要。除了一些native方法是基于本地方法棧實現的,所有的Java方法幾乎都是通Java虛擬機棧來實現方法的調用和執行過程(當然,需要程序計數器、堆、方法區的配合),所以Java虛擬機棧是虛擬機執行引擎的核心之一。而Java虛擬機棧中出棧入棧的元素就稱為「棧幀」。

0.棧幀的概念

棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執行的數據結構。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。每一個方法從調用至執行完成的過程,都對應著一個棧幀在虛擬機棧里從入棧到出棧的過程。

一個線程中方法的調用鏈可能會很長,很多方法都同時處于執行狀態。對于JVM執行引擎來說,在在活動線程中,只有位于JVM虛擬機棧棧頂的元素才是有效的,即稱為當前棧幀,與這個棧幀相關連的方法稱為當前方法,定義這個方法的類叫做當前類

執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。如果當前方法調用了其他方法,或者當前方法執行結束,那這個方法的棧幀就不再是當前棧幀了。

調用新的方法時,新的棧幀也會隨之創建。并且隨著程序控制權轉移到新方法,新的棧幀成為了當前棧幀。方法返回之際,原棧幀會返回方法的執行結果給之前的棧幀(返回給方法調用者),隨后虛擬機將會丟棄此棧幀。

棧幀是線程本地的私有數據,不可能在一個棧幀中引用另外一個線程的棧幀。

在概念模型上,典型的棧幀結構如下:

圖片來源:http://www.th7.cn/Program/java/201601/749326.shtml

關于「棧幀」,我們在看看《Java虛擬機規范》中的描述:

棧幀是用來存儲數據和部分過程結果的數據結構,同時也用來處理動態連接、方法返回值和異常分派。
棧幀隨著方法調用而創建,隨著方法結束而銷毀——無論方法正常完成還是異常完成都算作方法結束。
棧幀的存儲空間由創建它的線程分配在Java虛擬機棧之中,每一個棧幀都有自己的本地變量表(局部變量表)、操作數棧和指向當前方法所屬的類的運行時常量池的引用。

接下來,詳細講解一下棧幀中的局部變量表、操作數棧、動態連接、方法返回地址等各個部分的數據結構和作用。

1.局部變量表

局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數和方法內定義的局部變量。局部變量表的容量以變量槽(Variable Slot)為最小單位,Java虛擬機規范并沒有定義一個槽所應該占用內存空間的大小,但是規定了一個槽應該可以存放一個32位以內的數據類型。

在Java程序編譯為Class文件時,就在方法的Code屬性中的max_locals數據項中確定了該方法所需分配的局部變量表的最大容量。(最大Slot數量)

一個局部變量可以保存一個類型為boolean、byte、char、short、int、float、reference和returnAddress類型的數據。reference類型表示對一個對象實例的引用。returnAddress類型是為jsr、jsr_w和ret指令服務的,目前已經很少使用了。

虛擬機通過索引定位的方法查找相應的局部變量,索引的范圍是從0~局部變量表最大容量。如果Slot是32位的,則遇到一個64位數據類型的變量(如long或double型),則會連續使用兩個連續的Slot來存儲。

2.操作數棧

操作數棧(Operand Stack)也常稱為操作棧,它是一個后入先出棧(LIFO)。同局部變量表一樣,操作數棧的最大深度也在編譯的時候寫入到方法的Code屬性的max_stacks數據項中。

操作數棧的每一個元素可以是任意Java數據類型,32位的數據類型占一個棧容量,64位的數據類型占2個棧容量,且在方法執行的任意時刻,操作數棧的深度都不會超過max_stacks中設置的最大值。

當一個方法剛剛開始執行時,其操作數棧是空的,隨著方法執行和字節碼指令的執行,會從局部變量表或對象實例的字段中復制常量或變量寫入到操作數棧,再隨著計算的進行將棧中元素出棧到局部變量表或者返回給方法調用者,也就是出棧/入棧操作。一個完整的方法執行期間往往包含多個這樣出棧/入棧的過程。

3.動態連接

在一個class文件中,一個方法要調用其他方法,需要將這些方法的符號引用轉化為其在內存地址中的直接引用,而符號引用存在于方法區中的運行時常量池。

Java虛擬機棧中,每個棧幀都包含一個指向運行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支持方法調用過程中的動態連接(Dynamic Linking)

這些符號引用一部分會在類加載階段或者第一次使用時就直接轉化為直接引用,這類轉化稱為靜態解析。另一部分將在每次運行期間轉化為直接引用,這類轉化稱為動態連接。

4.方法返回

當一個方法開始執行時,可能有兩種方式退出該方法:

  • 正常完成出口
  • 異常完成出口

正常完成出口是指方法正常完成并退出,沒有拋出任何異常(包括Java虛擬機異常以及執行時通過throw語句顯示拋出的異常)。如果當前方法正常完成,則根據當前方法返回的字節碼指令,這時有可能會有返回值傳遞給方法調用者(調用它的方法),或者無返回值。具體是否有返回值以及返回值的數據類型將根據該方法返回的字節碼指令確定。

異常完成出口是指方法執行過程中遇到異常,并且這個異常在方法體內部沒有得到處理,導致方法退出。

無論是Java虛擬機拋出的異常還是代碼中使用athrow指令產生的異常,只要在本方法的異常表中沒有搜索到相應的異常處理器,就會導致方法退出。

無論方法采用何種方式退出,在方法退出后都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在當前棧幀中保存一些信息,用來幫他恢復它的上層方法執行狀態。

方法退出過程實際上就等同于把當前棧幀出棧,因此退出可以執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓如調用者的操作數棧中,調整PC計數器的值以指向方法調用指令后的下一條指令。

一般來說,方法正常退出時,調用者的PC計數值可以作為返回地址,棧幀中可能保存此計數值。而方法異常退出時,返回地址是通過異常處理器表確定的,棧幀中一般不會保存此部分信息。

5.附加信息

虛擬機規范允許具體的虛擬機實現增加一些規范中沒有描述的信息到棧幀之中,例如和調試相關的信息,這部分信息完全取決于不同的虛擬機實現。在實際開發中,一般會把動態連接,方法返回地址與其他附加信息一起歸為一類,稱為棧幀信息。


一個字節碼指令以及操作數出棧/入棧過程的小實例

這個小例子是我之前在牛客網上做題碰到的,改了一點內容拿出來,還是挺有意思的~順便介紹下字節碼指令,以及對著實例講解棧幀中操作數出棧入棧的整個過程。

image

問題:這個程序運行后會輸出哪三個數字?(以及test1和test2函數中return和finally的執行情況?)

答案:
System.out.println(test1(num)) ---- 60
System.out.println(b); ---- 60
System.out.println(test2(num)); -----30
執行情況,且看下面講解

學Java時我們都知道:

1.執行完try中的語句后,無論是否有異常被catch到,finally中的語句都會被執行(除了exit以及其它異常外),所以finally中通常用于關閉流關閉連接等操作。

2.finally中如果有return語句,則會用finally中的語句覆蓋掉try/catch中的return。

    public static int test1(int a){
        try{
            a+=20;
            return a;
        } finally {
            a+=30;
            return a;
        }
    }

于是,在test1中,try塊中return時a的值為30,經過finally塊+30后,值變為60,再return就是返回了finally中的a,即60。于是第一個輸出為60,這個很簡單。

    public static int test2(int b){
        try{
            b+=20;
            return b;
        }finally {
            b+=30;
            System.out.println(b);
        }
    }

在test2中,try塊中經過計算后return的b值為30,finally中沒有返回語句,故return的b值以try中的b=30為準(即第三個輸出為30)。

try語句塊中return b=30這個比較好理解,但是接下來finally中又對b進行了+30的操作,那此時第二個輸出System.out.println(b);輸出的值是60???,還是30? !(好像都能說得通)。

答案是60,為什么?讓我們用javap -c FinallyTest.class看一下其字節碼,順便學習下字節碼指令和class文件中的各個屬性。


首先,第一個輸出——System.out.println(test1(num)) 值是60,這個比較簡單,就不細說了,我們主要講解一下test2方法和第二個、第三個輸出:System.out.println(b); System.out.println(test2(num));**

對照之前的文章:Java虛擬機—Class文件結構 我們首先解析一下字節碼文件中test2()方法及各個屬性的作用,然后再對照字節碼指令來詳細看一下整個過程。

1.test2()方法字節碼中的各個屬性:

public static int test2(int);
    descriptor: (I)I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iinc          0, 20
         3: iload_0
         4: istore_1
         5: iinc          0, 30
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0
        12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        15: iload_1
        16: ireturn
        17: astore_2
        18: iinc          0, 30
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_0
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: aload_2
        29: athrow
      Exception table:
         from    to  target type
             0     5    17   any
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 20: 5
        line 21: 8
        line 18: 15
        line 20: 17
        line 21: 21
        line 22: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0     b   I
      StackMapTable: number_of_entries = 1
        frame_type = 81 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]

方法描述符descriptor為:(I)I;

test2()方法的訪問標志flags:ACC_PUBLIC和ACC_STATIC表示此方法的修飾符有public和static。

屬性表attribute_info中的“Code”屬性:即為屬性表集合,包括了:代碼轉換后字節碼指令+Exceptiontable+LineNumberTable+LocalVariableTable+StackMapTable

Exceptiontable是異常表用于處理異常后的程序出口。

LineNumberTable:行號表,用于指示Java源碼行號和字節碼指令的對應關系

LocalVariableTable:局部變量表,用于存放運行期間和操作數棧交互(出棧/入棧)的局部變量

StackMapTable:JDK1.6后新增的屬性,供新的類型檢測器檢查和處理目標方法的局部變量和操作數棧所需的類型是否匹配

2.test2()方法中的字節碼執行過程

看完了上面的屬性,下面讓我們來一行行地看一遍字節碼指令的部分,關于指令描述可以參考之前的文章——Java虛擬機—字節碼指令初探 。我們

Code:
      stack=2, locals=3, args_size=1                                           局部變量表     操作數棧
         0: iinc          0, 20  //自增指令,位于局部變量表中0號位置的int型數值+20   30          null
         3: iload_0              //從局部變量表0號位置加載一個int型值到操作數棧      30           30
         4: istore_1             //從操作數棧頂出棧一個int型值存到局部變量表1號位置 30,30        null
         5: iinc          0, 30  //局部變量表中0號位置的int型數值加30              60,30        null
         8: getstatic     #2     //訪問類的靜態字段值。#2表示靜態字段位于運行時     60,30        null   
        11: iload_0              //從局部變量表0號位置加載一個int型值到操作數棧     60,30         60
        12: invokevirtual #3     //調用PrintStream類的實例方法——println輸出60     60,30        null            
        15: iload_1              //從局部變量表1號位置加載一個int型值到操作數棧     60,30        30
        16: ireturn              //返回一個int型數值(從棧頂)                      60, 30        null
        17: astore_2             
        18: iinc          0, 30  
        21: getstatic     #2     
        24: iload_0              
        25: invokevirtual #3     
        28: aload_2              
        29: athrow               //拋出異常,程序跳轉到異常處理器中,(Exception table)
     Exception table:
        from    to  target type
            0     5    17   any

如上,代碼中序號0的指令——0: iinc對應test2源碼中try塊中:b += 20。此處test2()方法是在主函數main()中被調用的,在main()方法棧幀中操作數出棧一個int型值10,作為test2()方法調用的參數。test2()方法調用時,會新構建test2方法的棧幀(從而成為當前棧幀),10作為參數就存到了當前棧幀的局部變量表0號位置。所以在0: iinc 0, 20執行時,test2()方法棧幀中局部變量表0號位置已經是有了10這個值的。然后,指令一行行地執行過程如上述注釋↑。

有幾點需要注意:

  • getstatic
  • invokevirtual
  • ireturn

getstatic指令用于訪問類的靜態字段值

以第一個getstatic指令為例,其后的參數#2,表示會在FinallyTest類的運行時常量池中2號位置查找此字段值。

Constant pool:
   #1 = Methodref          #7.#30         // java/lang/Object."<init>":()V
   #2 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #33.#34        // java/io/PrintStream.println:(I)V
   #4 = Methodref          #6.#35         // JustCoding/Practise/FinallyTest.test1:(I)I
   #5 = Methodref          #6.#36         // JustCoding/Practise/FinallyTest.test2:(I)I
   #6 = Class              #37            // JustCoding/Practise/FinallyTest
   #7 = Class              #38            // java/lang/Object
         .....
  #30 = NameAndType        #8:#9          // "<init>":()V
  #31 = Class              #40            // java/lang/System
  #32 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #33 = Class              #43            // java/io/PrintStream
  #34 = NameAndType        #44:#45        // println:(I)V
  #35 = NameAndType        #15:#16        // test1:(I)I
  #36 = NameAndType        #21:#16        // test2:(I)I
         .....

常量池中2號位置的字段值是符號引用,該引用指向的是常量池中31號和32號位置,即java/lang/System類和java/io/PrintStream類中println的方法描述。如果該靜態字段(#2號)所指向的類或接口沒有被初始化,則指令執行過程將觸發其初始化過程。

invokevirtual指令用于調用實例方法

此處調用java.io.PrintStream類中的println方法后,會自動從test2的操作數棧中出棧相應的參數(即60)。然后方法執行來到了println方法中,于是新建此方法的棧幀,將當前棧幀切換到println棧幀上,將參數60入棧,在完成println方法后(輸出60)再切換回到test2的棧幀中。

所以,第二個System.out.println(b);輸出的是60.**

iteturn指令用于從操作數棧頂出棧一個int型的值給方法調用者

看一下test2方法的Java代碼:

public static int test2(int b){
        try{
            b += 20;
            return b;
        }finally {
            b += 30;
            System.out.println(b);
        }
    }

按道理應該try塊中執行完b += 20后應該緊跟著就return了,不過由于finally塊的存在,還需要執行后面的內容,那么在其字節碼指令中是如何實現的呢?

b += 20對應0: iinc、3: iload_0、4: istore_1三條指令,之后執行了finally塊中的內容:

5: iinc .....直到16: ireturn才正式執行return操作。此時return的是局部變量表中1號位置暫存的值30,所以第三個System.out.println(test2(num));輸出的是30.**

最后,ireturn指令到throw之間的為什么會有一段指令?這個和finally塊的設計相關稍微有點復雜,暫時不講。


以下是完整的,javap -v FinallyTest.class后的字節碼:

image

接下來的文章中,還是會沿著《深入理解Java虛擬機》,講解更多精彩的內容,包括:

垃圾收集器和算法、Java中方法調用(重載、重寫)的底層實現、new一個對象和常量池方法區的關系、Java對象內存布局等等~敬請期待,喜歡請點個贊唄??

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容