一、棧幀
棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。
棧幀隨着方法調用而創建,隨着方法結束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。棧幀的存儲空間分配在Java虛擬機棧之中,每一個棧幀都有自己的局部變量表、操作數棧和指向當前方法所屬的類的運行時常量池的引用。
局部變量表和操作數棧的容量是在編譯期確定,並通過方法的Code屬性保存及提供給棧幀使用。因此,棧幀容量的大小僅僅取決於Java虛擬機的實現和方法調用時可被分配的內存。
在一條線程之中,只有目前正在執行的那個方法的棧幀是活動的。這個棧幀就被稱為是當前棧幀(Current Frame),這個棧幀對應的方法就被稱為是當前方法(Current Method),定義這個方法的類就稱作當前類(Current Class)。對局部變量表和操作數棧的各種操作,通常都指的是對當前棧幀的對局部變量表和操作數棧進行的操作。
如果當前方法調用了其他方法,或者當前方法執行結束,那這個方法的棧幀就不再是當前棧幀了。當一個新的方法被調用,一個新的棧幀也會隨之而創建,並且隨着程序控制權移交到新的方法而成為新的當前棧幀。當方法返回的之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,在方法返回之后,當前棧幀就隨之被丟棄,前一個棧幀就重新成為當前棧幀了。
棧幀是線程本地私有的數據,不可能在一個棧幀之中引用另外一條線程的棧幀。
二、局部變量表
每個棧幀內部都包含一組稱為局部變量表(Local Variables)的變量列表。棧幀中局部變量表的長度由編譯期決定,並且存儲於類和接口的二進制表示之中,既通過方法的Code屬性保存及提供給棧幀使用。
一個局部變量(Slot)可以保存一個類型為boolean、byte、char、short、float、reference和returnAddress的數據,兩個局部變量可以保存一個類型為long和double的數據。
局部變量使用索引來進行定位訪問,第一個局部變量的索引值為零,局部變量的索引值是從零至小於局部變量表最大容量的所有整數。
long和double類型的數據占用兩個連續的局部變量,這兩種類型的數據值采用兩個局部變量之中較小的索引值來定位。例如我們講一個double類型的值存儲在索引值為n的局部變量中,實際上的意思是索引值為n和n+1的兩個局部變量都用來存儲這個值。索引值為n+1的局部變量是無法直接讀取的,但是可能會被寫入,不過如果進行了這種操作,就將會導致局部變量n的內容失效掉。
上文中提及的局部變量n的n值並不要求一定是偶數,Java虛擬機也不要求double和long類型數據采用64位對齊的方式存放在連續的局部變量中。虛擬機實現者可以自由地選擇適當的方式,通過兩個局部變量來存儲一個double或long類型的值。
Java虛擬機使用局部變量表來完成方法調用時的參數傳遞,當一個方法被調用的時候,它的參數將會傳遞至從0開始的連續的局部變量表位置上。特別地,當一個實例方法被調用的時候,第0個局部變量一定是用來存儲被調用的實例方法所在的對象的引用(即Java語言中的“this”關鍵字)。后續的其他參數將會傳遞至從1開始的連續的局部變量表位置上。
三、操作數棧
每一個棧幀內部都包含一個稱為操作數棧(Operand Stack)的后進先出(Last-In-First-Out,LIFO)棧。棧幀中操作數棧的長度由編譯期決定,並且存儲於類和接口的二進制表示之中,既通過方法的Code屬性保存及提供給棧幀使用。
在上下文明確,不會產生誤解的前提下,我們經常把“當前棧幀的操作數棧”直接簡稱為“操作數棧”。
操作數棧所屬的棧幀在剛剛被創建的時候,操作數棧是空的。Java虛擬機提供一些字節碼指令來從局部變量表或者對象實例的字段中復制常量或變量值到操作數棧中,也提供了一些指令用於從操作數棧取走數據、操作數據和把操作結果重新入棧。在方法調用的時候,操作數棧也用來准備調用方法的參數以及接收方法返回結果。
舉個例子,iadd字節碼指令的作用是將兩個int類型的數值相加,它要求在執行的之前操作數棧的棧頂已經存在兩個由前面其他指令放入的int型數值。在iadd指令執行時,2個int值從操作棧中出棧,相加求和,然后將求和結果重新入棧。在操作數棧中,一項運算常由多個子運算(Subcomputations)嵌套進行,一個子運算過程的結果可以被其他外圍運算所使用。
每一個操作數棧的成員(Entry)可以保存一個Java虛擬機中定義的任意數據類型的值,包括long和double類型。
在操作數棧中的數據必須被正確地操作,這里正確操作是指對操作數棧的操作必須與操作數棧棧頂的數據類型相匹配,例如不可以入棧兩個int類型的數據,然后當作long類型去操作他們,或者入棧兩個float類型的數據,然后使用iadd指令去對它們進行求和。有一小部分Java虛擬機指令(例如dup和swap指令)可以不關注操作數的具體數據類型,把所有在運行時數據區中的數據當作裸類型(Raw Type)數據來操作,這些指令不可以用來修改數據,也不可以拆散那些原本不可拆分的數據,這些操作的正確性將會通過Class文件的校驗過程來強制保障。
在任意時刻,操作數棧都會有一個確定的棧深度,一個long或者double類型的數據會占用兩個單位的棧深度,其他數據類型則會占用一個單位深度。
分析:
有如下代碼:
- package cc.lixiaohui.demo;
- public class Foo {
- public static void main(String[] args) {
- int a = 1;
- int b = 2;
- int c = a + b;
- }
- }
利用javap工具生成虛擬機匯編代碼(java虛擬機指令集):
括號內的注釋我是自己加的,其余是javap生成的
- E:\EclipseWorkspace\demo-foo\target\classes\cc\lixiaohui\demo>javap -c -l Foo.class
- Compiled from "Foo.java"
- public class cc.lixiaohui.demo.Foo {
- public cc.lixiaohui.demo.Foo(); (//這是默認構造方法)
- Code:
- 0: aload_0
- 1: invokespecial #8 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 3: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this Lcc/lixiaohui/demo/Foo;
- public static void main(java.lang.String[]);
- Code:
- 0: iconst_1 (//把int值1壓入操作數棧)
- 1: istore_1 (//把棧頂值存儲到局部變量表下標為1的位置)
- 2: iconst_2 (//把int值2壓入操作數棧)
- 3: istore_2 (//把棧頂值存儲到局部變量表下標為2的位置)
- 4: iload_1 (//取局部變量表中下標為1的變量壓棧)
- 5: iload_2 (//取局部變量表中下標為2的變量壓棧)
- 6: iadd (//從操作數棧中彈出兩個int值進行相加操作,相加的結果壓棧)
- 7: istore_3 (//把棧頂值存儲到局部變量表下標為3的位置)
- 8: return
- LineNumberTable: (//這是java代碼行與該棧幀指令代碼行的映射)
- line 5: 0 (//java代碼第五行為“int a = 1”,對應的虛擬機匯編代碼為“iconst_1”)
- line 6: 2
- line 7: 4
- line 8: 8
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 9 0 args [Ljava/lang/String;
- 2 7 1 a I
- 4 5 2 b I
- 8 1 3 c I
- }
可以看到執行過程中不斷在局部變量表和操作數棧間來回傳遞數據。