
前言:
在之前的文章:Java虛擬機—堆、棧、運行時數(shù)據(jù)區(qū) 中,我們整體介紹了JVM在運行時的一些數(shù)據(jù)區(qū)域如堆、方法區(qū)、程序計數(shù)器、虛擬機棧、本地方法棧。本篇文章,我們圍繞其中的一個區(qū)域展開——虛擬機棧中的棧元素棧幀
所以,本文的主要分為兩部分:
1.Java虛擬機運行時棧幀介紹 2.一個關(guān)于字節(jié)碼指令以及操作數(shù)出棧/入棧過程的小實例
其中,運行時棧幀介紹主要包括:
- 0.棧幀的概念
- 1.局部變量表
- 2.操作數(shù)棧
- 3.動態(tài)鏈接
- 4.方法返回
- 5.附加信息
Java虛擬機棧和運行時棧幀結(jié)構(gòu)
Java虛擬機是基于「?!辜軜?gòu)的,如圖所示:

為什么要深入研究虛擬機棧呢?因為它hin重要。除了一些native方法是基于本地方法棧實現(xiàn)的,所有的Java方法幾乎都是通Java虛擬機棧來實現(xiàn)方法的調(diào)用和執(zhí)行過程(當然,需要程序計數(shù)器、堆、方法區(qū)的配合),所以Java虛擬機棧是虛擬機執(zhí)行引擎的核心之一。而Java虛擬機棧中出棧入棧的元素就稱為「棧幀」。
0.棧幀的概念
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個方法從調(diào)用至執(zhí)行完成的過程,都對應(yīng)著一個棧幀在虛擬機棧里從入棧到出棧的過程。
一個線程中方法的調(diào)用鏈可能會很長,很多方法都同時處于執(zhí)行狀態(tài)。對于JVM執(zhí)行引擎來說,在在活動線程中,只有位于JVM虛擬機棧棧頂?shù)脑夭攀怯行У模捶Q為當前棧幀,與這個棧幀相關(guān)連的方法稱為當前方法,定義這個方法的類叫做當前類。
執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當前棧幀進行操作。如果當前方法調(diào)用了其他方法,或者當前方法執(zhí)行結(jié)束,那這個方法的棧幀就不再是當前棧幀了。
調(diào)用新的方法時,新的棧幀也會隨之創(chuàng)建。并且隨著程序控制權(quán)轉(zhuǎn)移到新方法,新的棧幀成為了當前棧幀。方法返回之際,原棧幀會返回方法的執(zhí)行結(jié)果給之前的棧幀(返回給方法調(diào)用者),隨后虛擬機將會丟棄此棧幀。
棧幀是線程本地的私有數(shù)據(jù),不可能在一個棧幀中引用另外一個線程的棧幀。
在概念模型上,典型的棧幀結(jié)構(gòu)如下:

關(guān)于「棧幀」,我們在看看《Java虛擬機規(guī)范》中的描述:
棧幀是用來存儲數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時也用來處理動態(tài)連接、方法返回值和異常分派。
棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無論方法正常完成還是異常完成都算作方法結(jié)束。
棧幀的存儲空間由創(chuàng)建它的線程分配在Java虛擬機棧之中,每一個棧幀都有自己的本地變量表(局部變量表)、操作數(shù)棧和指向當前方法所屬的類的運行時常量池的引用。
接下來,詳細講解一下棧幀中的局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址等各個部分的數(shù)據(jù)結(jié)構(gòu)和作用。
1.局部變量表
局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)定義的局部變量。局部變量表的容量以變量槽(Variable Slot)為最小單位,Java虛擬機規(guī)范并沒有定義一個槽所應(yīng)該占用內(nèi)存空間的大小,但是規(guī)定了一個槽應(yīng)該可以存放一個32位以內(nèi)的數(shù)據(jù)類型。
在Java程序編譯為Class文件時,就在方法的Code屬性中的max_locals數(shù)據(jù)項中確定了該方法所需分配的局部變量表的最大容量。(最大Slot數(shù)量)
一個局部變量可以保存一個類型為boolean、byte、char、short、int、float、reference和returnAddress類型的數(shù)據(jù)。reference類型表示對一個對象實例的引用。returnAddress類型是為jsr、jsr_w和ret指令服務(wù)的,目前已經(jīng)很少使用了。
虛擬機通過索引定位的方法查找相應(yīng)的局部變量,索引的范圍是從0~局部變量表最大容量。如果Slot是32位的,則遇到一個64位數(shù)據(jù)類型的變量(如long或double型),則會連續(xù)使用兩個連續(xù)的Slot來存儲。
2.操作數(shù)棧
操作數(shù)棧(Operand Stack)也常稱為操作棧,它是一個后入先出棧(LIFO)。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時候?qū)懭氲椒椒ǖ腃ode屬性的max_stacks數(shù)據(jù)項中。
操作數(shù)棧的每一個元素可以是任意Java數(shù)據(jù)類型,32位的數(shù)據(jù)類型占一個棧容量,64位的數(shù)據(jù)類型占2個棧容量,且在方法執(zhí)行的任意時刻,操作數(shù)棧的深度都不會超過max_stacks中設(shè)置的最大值。
當一個方法剛剛開始執(zhí)行時,其操作數(shù)棧是空的,隨著方法執(zhí)行和字節(jié)碼指令的執(zhí)行,會從局部變量表或?qū)ο髮嵗淖侄沃袕?fù)制常量或變量寫入到操作數(shù)棧,再隨著計算的進行將棧中元素出棧到局部變量表或者返回給方法調(diào)用者,也就是出棧/入棧操作。一個完整的方法執(zhí)行期間往往包含多個這樣出棧/入棧的過程。
3.動態(tài)連接
在一個class文件中,一個方法要調(diào)用其他方法,需要將這些方法的符號引用轉(zhuǎn)化為其在內(nèi)存地址中的直接引用,而符號引用存在于方法區(qū)中的運行時常量池。
Java虛擬機棧中,每個棧幀都包含一個指向運行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。
這些符號引用一部分會在類加載階段或者第一次使用時就直接轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為靜態(tài)解析。另一部分將在每次運行期間轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為動態(tài)連接。
4.方法返回
當一個方法開始執(zhí)行時,可能有兩種方式退出該方法:
- 正常完成出口
- 異常完成出口
正常完成出口是指方法正常完成并退出,沒有拋出任何異常(包括Java虛擬機異常以及執(zhí)行時通過throw語句顯示拋出的異常)。如果當前方法正常完成,則根據(jù)當前方法返回的字節(jié)碼指令,這時有可能會有返回值傳遞給方法調(diào)用者(調(diào)用它的方法),或者無返回值。具體是否有返回值以及返回值的數(shù)據(jù)類型將根據(jù)該方法返回的字節(jié)碼指令確定。
異常完成出口是指方法執(zhí)行過程中遇到異常,并且這個異常在方法體內(nèi)部沒有得到處理,導(dǎo)致方法退出。
無論是Java虛擬機拋出的異常還是代碼中使用athrow指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到相應(yīng)的異常處理器,就會導(dǎo)致方法退出。
無論方法采用何種方式退出,在方法退出后都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在當前棧幀中保存一些信息,用來幫他恢復(fù)它的上層方法執(zhí)行狀態(tài)。
方法退出過程實際上就等同于把當前棧幀出棧,因此退出可以執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓如調(diào)用者的操作數(shù)棧中,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后的下一條指令。
一般來說,方法正常退出時,調(diào)用者的PC計數(shù)值可以作為返回地址,棧幀中可能保存此計數(shù)值。而方法異常退出時,返回地址是通過異常處理器表確定的,棧幀中一般不會保存此部分信息。
5.附加信息
虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范中沒有描述的信息到棧幀之中,例如和調(diào)試相關(guān)的信息,這部分信息完全取決于不同的虛擬機實現(xiàn)。在實際開發(fā)中,一般會把動態(tài)連接,方法返回地址與其他附加信息一起歸為一類,稱為棧幀信息。
一個字節(jié)碼指令以及操作數(shù)出棧/入棧過程的小實例
這個小例子是我之前在??途W(wǎng)上做題碰到的,改了一點內(nèi)容拿出來,還是挺有意思的~順便介紹下字節(jié)碼指令,以及對著實例講解棧幀中操作數(shù)出棧入棧的整個過程。

問題:這個程序運行后會輸出哪三個數(shù)字?(以及test1和test2函數(shù)中return和finally的執(zhí)行情況?)
答案:
System.out.println(test1(num)) ---- 60
System.out.println(b); ---- 60
System.out.println(test2(num)); -----30
執(zhí)行情況,且看下面講解
學(xué)Java時我們都知道:
1.執(zhí)行完try中的語句后,無論是否有異常被catch到,finally中的語句都會被執(zhí)行(除了exit以及其它異常外),所以finally中通常用于關(guān)閉流關(guān)閉連接等操作。
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,經(jīng)過finally塊+30后,值變?yōu)?0,再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塊中經(jīng)過計算后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看一下其字節(jié)碼,順便學(xué)習下字節(jié)碼指令和class文件中的各個屬性。
首先,第一個輸出——System.out.println(test1(num)) 值是60,這個比較簡單,就不細說了,我們主要講解一下test2方法和第二個、第三個輸出:System.out.println(b); System.out.println(test2(num));**
對照之前的文章:Java虛擬機—Class文件結(jié)構(gòu) 我們首先解析一下字節(jié)碼文件中test2()方法及各個屬性的作用,然后再對照字節(jié)碼指令來詳細看一下整個過程。
1.test2()方法字節(jié)碼中的各個屬性:
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”屬性:即為屬性表集合,包括了:代碼轉(zhuǎn)換后字節(jié)碼指令+Exceptiontable+LineNumberTable+LocalVariableTable+StackMapTable
Exceptiontable是異常表用于處理異常后的程序出口。
LineNumberTable:行號表,用于指示Java源碼行號和字節(jié)碼指令的對應(yīng)關(guān)系
LocalVariableTable:局部變量表,用于存放運行期間和操作數(shù)棧交互(出棧/入棧)的局部變量
StackMapTable:JDK1.6后新增的屬性,供新的類型檢測器檢查和處理目標方法的局部變量和操作數(shù)棧所需的類型是否匹配
2.test2()方法中的字節(jié)碼執(zhí)行過程
看完了上面的屬性,下面讓我們來一行行地看一遍字節(jié)碼指令的部分,關(guān)于指令描述可以參考之前的文章——Java虛擬機—字節(jié)碼指令初探 。我們
Code:
stack=2, locals=3, args_size=1 局部變量表 操作數(shù)棧
0: iinc 0, 20 //自增指令,位于局部變量表中0號位置的int型數(shù)值+20 30 null
3: iload_0 //從局部變量表0號位置加載一個int型值到操作數(shù)棧 30 30
4: istore_1 //從操作數(shù)棧頂出棧一個int型值存到局部變量表1號位置 30,30 null
5: iinc 0, 30 //局部變量表中0號位置的int型數(shù)值加30 60,30 null
8: getstatic #2 //訪問類的靜態(tài)字段值。#2表示靜態(tài)字段位于運行時 60,30 null
11: iload_0 //從局部變量表0號位置加載一個int型值到操作數(shù)棧 60,30 60
12: invokevirtual #3 //調(diào)用PrintStream類的實例方法——println輸出60 60,30 null
15: iload_1 //從局部變量表1號位置加載一個int型值到操作數(shù)棧 60,30 30
16: ireturn //返回一個int型數(shù)值(從棧頂) 60, 30 null
17: astore_2
18: iinc 0, 30
21: getstatic #2
24: iload_0
25: invokevirtual #3
28: aload_2
29: athrow //拋出異常,程序跳轉(zhuǎn)到異常處理器中,(Exception table)
Exception table:
from to target type
0 5 17 any
如上,代碼中序號0的指令——0: iinc對應(yīng)test2源碼中try塊中:b += 20。此處test2()方法是在主函數(shù)main()中被調(diào)用的,在main()方法棧幀中操作數(shù)出棧一個int型值10,作為test2()方法調(diào)用的參數(shù)。test2()方法調(diào)用時,會新構(gòu)建test2方法的棧幀(從而成為當前棧幀),10作為參數(shù)就存到了當前棧幀的局部變量表0號位置。所以在0: iinc 0, 20執(zhí)行時,test2()方法棧幀中局部變量表0號位置已經(jīng)是有了10這個值的。然后,指令一行行地執(zhí)行過程如上述注釋↑。
有幾點需要注意:
- getstatic
- invokevirtual
- ireturn
getstatic指令用于訪問類的靜態(tài)字段值
以第一個getstatic指令為例,其后的參數(shù)#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的方法描述。如果該靜態(tài)字段(#2號)所指向的類或接口沒有被初始化,則指令執(zhí)行過程將觸發(fā)其初始化過程。
invokevirtual指令用于調(diào)用實例方法
此處調(diào)用java.io.PrintStream類中的println方法后,會自動從test2的操作數(shù)棧中出棧相應(yīng)的參數(shù)(即60)。然后方法執(zhí)行來到了println方法中,于是新建此方法的棧幀,將當前棧幀切換到println棧幀上,將參數(shù)60入棧,在完成println方法后(輸出60)再切換回到test2的棧幀中。
所以,第二個System.out.println(b);輸出的是60.**
iteturn指令用于從操作數(shù)棧頂出棧一個int型的值給方法調(diào)用者
看一下test2方法的Java代碼:
public static int test2(int b){
try{
b += 20;
return b;
}finally {
b += 30;
System.out.println(b);
}
}
按道理應(yīng)該try塊中執(zhí)行完b += 20后應(yīng)該緊跟著就return了,不過由于finally塊的存在,還需要執(zhí)行后面的內(nèi)容,那么在其字節(jié)碼指令中是如何實現(xiàn)的呢?
b += 20對應(yīng)0: iinc、3: iload_0、4: istore_1三條指令,之后執(zhí)行了finally塊中的內(nèi)容:
5: iinc .....直到16: ireturn才正式執(zhí)行return操作。此時return的是局部變量表中1號位置暫存的值30,所以第三個System.out.println(test2(num));輸出的是30.**
最后,ireturn指令到throw之間的為什么會有一段指令?這個和finally塊的設(shè)計相關(guān)稍微有點復(fù)雜,暫時不講。
以下是完整的,javap -v FinallyTest.class后的字節(jié)碼:

接下來的文章中,還是會沿著《深入理解Java虛擬機》,講解更多精彩的內(nèi)容,包括:
垃圾收集器和算法、Java中方法調(diào)用(重載、重寫)的底層實現(xiàn)、new一個對象和常量池方法區(qū)的關(guān)系、Java對象內(nèi)存布局等等~敬請期待,喜歡請點個贊唄??