1.JAVA虛擬機(jī)執(zhí)行模型
在JVM執(zhí)行模型里,每個(gè)方法都是在線程中執(zhí)行,而每個(gè)線程對(duì)應(yīng)自己的棧,每個(gè)棧由幀組成。每個(gè)幀對(duì)應(yīng)一個(gè)方法調(diào)用,每次調(diào)用一個(gè)方法,
會(huì)將新幀壓入當(dāng)前線程的執(zhí)行棧,當(dāng)方法返回時(shí)(異常退出也是返回),再將這個(gè)幀從執(zhí)行棧彈出。
每個(gè)幀主要包括兩部分,一個(gè)局部變量表和一個(gè)操作數(shù)棧,關(guān)系如下圖所示:

這里注意,局部變量表是根據(jù)索引訪問(wèn)的列表,類(lèi)似數(shù)組;而操作數(shù)棧則是“后入先出”的棧,這里非常重要,因?yàn)閖ava函數(shù)的字節(jié)碼指令基本上都是對(duì)這兩個(gè)數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作。
局部變量表和操作數(shù)棧的大小取決于方法代碼,在編譯時(shí)計(jì)算,并隨字節(jié)碼指令一起寫(xiě)入class文件中,
public int gogo() {
Log.i("zkw", "hello");
return 888;
}
這是一個(gè)java方法,編譯成class之后內(nèi)容如下:
// access flags 0x1
public gogo()I
LDC "zkw"
LDC "hello"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
SIPUSH 888
IRETURN
MAXSTACK = 2
MAXLOCALS = 1
最下面兩行的MAXSTACK和MAXLOCALS的值就是操作數(shù)棧和局部變量表的大小。
局部變量表和操作數(shù)棧中的每個(gè)槽(slot)可以保存除long和double之外的任意java值,而long和double需要兩個(gè)槽,比如向局部變量表儲(chǔ)存一個(gè)int和一個(gè)long,則表中第一個(gè)位置是int值,第二和第三個(gè)位置存的是long值。
還有一點(diǎn)需要注意,如果是非靜態(tài)方法,局部變量表的第0個(gè)位置為"this"。
2.字節(jié)代碼指令
Java類(lèi)型被編譯成class后,都是用類(lèi)型描述符表示的,如下圖:

方法也同樣會(huì)被編譯成方法描述符,如下:

字節(jié)碼指令是由操作碼和參數(shù)組成:
- 操作碼是一個(gè)字節(jié)代碼名,由助記符號(hào)表示,例如操作碼0,對(duì)應(yīng)的是NOP,表示無(wú)任何操作的指令;操作碼21,對(duì)應(yīng)ILOAD,表示讀取局部變量表某個(gè)位置的int值。
- 參數(shù)是儲(chǔ)存在編譯后代碼中的靜態(tài)值。
字節(jié)碼指令分為兩種:
- 一種是用來(lái)在局部變量表和操作數(shù)棧之間傳送值的。比如FSTORE i指令從操作數(shù)棧彈出一個(gè)float值,并存入索引i對(duì)應(yīng)的局部變量表中。而DLOAD j指令則是讀取局部變量表中索引j和j+1對(duì)應(yīng)的double值(思考一下為什么是j和j+1),并將它壓入操作數(shù)棧。
- 另一部分字節(jié)碼指令僅用來(lái)處理操作數(shù)棧。比如xADD(x對(duì)應(yīng)I、L、F、D)指令從操作數(shù)棧彈出兩個(gè)數(shù)值做加法,然后將結(jié)果壓入棧。再比如INVOKESTATIC用于調(diào)用靜態(tài)方法,該指令會(huì)從操作數(shù)棧彈出n+1個(gè)值(n是靜態(tài)方法的n個(gè)參數(shù),+1對(duì)應(yīng)目標(biāo)對(duì)象),并壓回方法調(diào)用的結(jié)果。
還是用上面的代碼舉例子,我們直接看字節(jié)碼:
// access flags 0x1
public gogo()I
LDC "zkw"
LDC "hello"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
SIPUSH 888
IRETURN
MAXSTACK = 2
MAXLOCALS = 1
LDC是將參數(shù)中的值壓入操作數(shù)棧,所以前兩行執(zhí)行完,操作數(shù)棧應(yīng)該長(zhǎng)這樣[...,"zkw","hello"],前面...是之前壓入的值,
然后INVOKESTATIC指令彈出之前壓入的參數(shù),然后調(diào)用Log.i靜態(tài)方法,最后將int結(jié)果壓入棧,此時(shí)操作數(shù)棧應(yīng)該長(zhǎng)這樣[...,int結(jié)果]
由于沒(méi)有使用Log.i的返回值,所以直接將返回值從操作數(shù)棧POP出去,
接下來(lái)SIPUSH將888壓入操作數(shù)棧,此時(shí)棧長(zhǎng)這樣[...,888]
然后IRETURN從操作數(shù)棧彈出int值并返回,方法調(diào)用結(jié)束。
這里我們沒(méi)有看到對(duì)局部變量表的操作,下面稍微修改下gogo方法:
public int gogo() {
int a = Log.i("zkw", "hello");
return a;
}
為了看到如何操作局部變量表,我們獲取Log.i返回的int值,并將其return,編譯之后如下:
// access flags 0x1
public gogo()I
LDC "zkw"
LDC "hello"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
ISTORE 1
ILOAD 1
IRETURN
MAXSTACK = 2
MAXLOCALS = 2
當(dāng)INVOKESTATIC指令執(zhí)行之后,操作數(shù)棧為[...,int值],局部變量表為[this]
看到INVOKESTATIC之后,多了個(gè)ISTORE指令,ISTORE 1指令是彈出操作數(shù)棧棧頂?shù)闹?也就是log.i的返回值),將其存入局部變量表索引為1的位置(思考一下為什么不是0),當(dāng)ISTORE執(zhí)行完,操作數(shù)棧為[...],局部變量表為[this,int值]。
然后執(zhí)行ILOAD 1,該指令取出局部變量表1位置的值,并壓入操作數(shù)棧,此時(shí)操作數(shù)棧為[...int值],局部變量表為[this]。
然后IRETURN從操作數(shù)棧彈出int值,并將其return,執(zhí)行結(jié)束。
3.棧映射幀
java1.6之后還引入了棧映射幀,用于加快虛擬機(jī)中類(lèi)驗(yàn)證過(guò)程的速度。這個(gè)映射幀主要記錄每個(gè)指令執(zhí)行前的局部變量表和操作數(shù)棧中包含的類(lèi)型狀態(tài)。這個(gè)幀和所謂的棧幀沒(méi)有關(guān)系,這個(gè)映射幀僅僅標(biāo)示當(dāng)前局部變量表和操作數(shù)棧的狀態(tài)。
當(dāng)jvm進(jìn)入一個(gè)方法時(shí),根據(jù)方法描述符就可以確定初始幀的狀態(tài),例如方法com.demo.Foo.gogo(int a)的局部變量表的初始狀態(tài)為[com.demo.Foo, I],而操作數(shù)棧初始狀態(tài)肯定是空的。所以這個(gè)方法的初始幀為[com.demo.Foo, I],[]
為了節(jié)省空間,編譯方法時(shí)并不會(huì)為每條指令生成一個(gè)映射幀,事實(shí)上,它僅為跳轉(zhuǎn)指令(包括if else,try cache等)生成映射幀。
為了節(jié)省更多空間,對(duì)每個(gè)需要生成映射幀的地方做壓縮,僅僅儲(chǔ)存與前一幀的差別,比如與前一幀的狀態(tài)一樣時(shí),使用F_SAME助記符,當(dāng)比前一幀增加了3個(gè)以?xún)?nèi)的局部變量時(shí),使用F_APPEND [],當(dāng)增加了3個(gè)以上的局部變量時(shí),使用F_FULL []。說(shuō)了這么多可能有點(diǎn)暈了,看例子吧。
我們修改上面的例子,增加一些局部變量和條件判斷:
public int gogo(int c) {
int a = Log.i("zkw", "hello");
float f = 0.4f;
if (a > 0) {
Log.i("zkw", ">>0");
} else {
Log.i("zkw", "<<0");
}
return a;
}
代碼中增加了兩個(gè)局部變量a和f,看看編譯后的字節(jié)碼:
// access flags 0x1
public gogo(I)I
LDC "zkw"
LDC "hello"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
ISTORE 2
LDC 0.4
FSTORE 3
ILOAD 2
IFLE L0
LDC "zkw"
LDC ">>0"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
GOTO L1
L0
FRAME APPEND [I F]
LDC "zkw"
LDC "<<0"
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L1
FRAME SAME
ILOAD 2
IRETURN
MAXSTACK = 2 MAXLOCALS = 4
我們假定這個(gè)方法是com.demo.Foo類(lèi)的,那么這個(gè)方法的初始幀狀態(tài)應(yīng)該是[com.demo.Foo, I],[],字節(jié)碼中不會(huì)標(biāo)示初始幀狀態(tài)。
然后代碼繼續(xù)往下走,我們?cè)黾恿藘蓚€(gè)局部變量int a和float f,所以幀狀態(tài)出現(xiàn)變化,這個(gè)變化會(huì)在第一個(gè)跳轉(zhuǎn)目標(biāo)里展示出來(lái),請(qǐng)看L0下面的FRAME APPEND [I F],意思是相比于之前的幀狀態(tài)增加了兩個(gè)局部變量,類(lèi)型是int和float,此時(shí)幀狀態(tài)更新成[com.demo.Foo, I, I, F],[]。
之后遇見(jiàn)了下一個(gè)跳轉(zhuǎn)目標(biāo)L1,這時(shí)候的局部變量沒(méi)有變化,所以使用FRAME SAME標(biāo)示。
這些FRAME指令僅僅是標(biāo)示幀狀態(tài)的變化,沒(méi)有對(duì)局部變量表和操作數(shù)棧做任何操作,目的是加快java虛擬機(jī)中類(lèi)驗(yàn)證過(guò)程的速度。
之前說(shuō)F_APPEND是標(biāo)示增加3個(gè)之內(nèi)的幀變化,那3個(gè)之外呢,我們繼續(xù)修改gogo方法,增加兩個(gè)局部變量:
public int gogo(int c) {
int a = Log.i("zkw", "hello");
float f = 0.4f;
short s = 12;
long l = 10003983839L;
if (a > 0) {
Log.i("zkw", ">>0");
} else {
Log.i("zkw", "<<0");
}
return a;
}
看到我們?cè)黾恿藄hort s和long l,看看編譯后啥樣:
// access flags 0x1
public gogo(I)I
LDC "zkw" LDC "hello" INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
ISTORE 2 LDC 0.4 FSTORE 3 BIPUSH 12 ISTORE 4 LDC 10003983839 LSTORE 5 ILOAD 2 IFLE L0
LDC "zkw" LDC ">>0" INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
GOTO L1
L0
FRAME FULL [com/demo/Foo I I F I J] []
LDC "zkw" LDC "<<0" INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L1
FRAME SAME
ILOAD 2 IRETURN
MAXSTACK = 2 MAXLOCALS = 7
看到標(biāo)紅的那行,使用了FRAME FULL的指令,后面參數(shù)就是完全的局部變量表狀態(tài)。