Java byte code 的學習意義
為啥要學java bytecode,這就跟你問我已經(jīng)會python了為啥要學匯編一個道理。
我為啥要整理這些難記的知識 ? 因為公司用到了某為的一個據(jù)說極其牛逼的技術框架,但是某為不給源碼,導致理解總是膚淺的。
要了解應用,唯有剝開拆散了研究一番,于是JD就上了,但是還是有些代碼反編譯不出來,這時候 javap 就有用了,從指令級回推代碼。
先轉一份IBM牛人的筆記: http://it.deepinmind.com
Java字節(jié)碼淺析(—)
英文原文鏈接,譯文鏈接,原文作者:James Bloom,譯者:有孚
明白Java代碼是如何編譯成字節(jié)碼并在JVM上運行的非常重要,這有助于理解程序運行的時候究竟發(fā)生了些什么。理解這點不僅能搞清語言特性是如何實現(xiàn)的,并且在做方案討論的時候能清楚相應的副作用及權衡利弊。
本文介紹了Java代碼是如何編譯成字節(jié)碼并在JVM上執(zhí)行的。想了解JVM的內(nèi)部結構以及字節(jié)碼運行時用到的各個內(nèi)存區(qū)域,可以看下我前面的一篇關于JVM內(nèi)部細節(jié)的文章。
本文分為三部分,每一部分都分成幾個小節(jié)。每個小節(jié)都可以單獨閱讀,不過由于一些概念是逐步建立起來的,如果你依次閱讀完所有章節(jié)會更簡單一些。每一節(jié)都會覆蓋到Java代碼中的不同結構,并詳細介紹了它們是如何編譯并執(zhí)行的。
- 第一部分, 基礎概念
變量
局部變量
JVM是一個基于棧的架構。方法執(zhí)行的時候(包括main方法),在棧上會分配一個新的幀,這個棧幀包含一組局部變量。這組局部變量包含了方法運行過程中用到的所有變量,包括this引用,所有的方法參數(shù),以及其它局部定義的變量。對于類方法(也就是static方法)來說,方法參數(shù)是從第0個位置開始的,而對于實例方法來說,第0個位置上的變量是this指針。
局部變量可以是以下這些類型:
- char
- long
- short
- int
- float
- double
- 引用
- 返回地址
除了long和double類型外,每個變量都只占局部變量區(qū)中的一個變量槽(slot),而long及double會占用兩個連續(xù)的變量槽,因為這些類型是64位的。
當一個新的變量創(chuàng)建的時候,操作數(shù)棧(operand stack)會用來存儲這個新變量的值。然后這個變量會存儲到局部變量區(qū)中對應的位置上。如果這個變量不是基礎類型的話,本地變量槽上存的就只是一個引用。這個引用指向堆的里一個對象。
比如:
int i = 5;
編譯后就成了
0: bipush 5
2: istore_0
bipush
用來將一個字節(jié)作為整型數(shù)字壓入操作數(shù)棧中,在這里5就會被壓入操作數(shù)棧上。
istore_0
這是istore_這組指令集(譯注:嚴格來說,這個應該叫做操作碼,opcode ,指令是指操作碼加上對應的操作數(shù),oprand。不過操作碼一般作為指令的助記符,這里統(tǒng)稱為指令)中的一條,這組指令是將一個整型數(shù)字存儲到本地變量中。n代表的是局部變量區(qū)中的位置,并且只能是0,1,2,3。再多的話只能用另一條指令istore了,這條指令會接受一個操作數(shù),對應的是局部變量區(qū)中的位置信息。
這條指令執(zhí)行的時候,內(nèi)存布局是這樣的:
class文件中的每一個方法都會包含一個局部變量表,如果這段代碼在一個方法里面的話,你會在類文件的局部變量表中發(fā)現(xiàn)如下的一條記錄。
LocalVariableTable:
Start Length Slot Name Signature
0 1 1 i I
字段
Java類里面的字段是作為類對象實例的一部分,存儲在堆里面的(類變量對應存儲在類對象里面)。關于字段的信息會添加到類文件里的field_info數(shù)組里,像下面這樣:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
另外,如果變量被初始化了,那么初始化的字節(jié)碼會加到構造方法里。
下面這段代碼編譯了之后:
public class SimpleClass {
public int simpleField = 100;
}
如果你用javap進行反編譯,這個被添加到了field_info數(shù)組里的字段會多出一段描述信息。
public int simpleField;
Signature: I
flags: ACC_PUBLIC
初始化變量的字節(jié)碼會被加到構造方法里,像下面這樣:
public SimpleClass();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field simpleField:I
10: return
aload_0
從局部變量數(shù)組中加載一個對象引用到操作數(shù)棧的棧頂。盡管這段代碼看起來沒有構造方法,但是在編譯器生成的默認的構造方法里,就會包含這段初始化的代碼。第一個局部變量正好是this引用,于是aload_0把this引用壓到操作數(shù)棧中。aload_0是aload_指令集中的一條,這組指令會將引用加載到操作數(shù)棧中。n對應的是局部變量數(shù)組中的位置,并且也只能是0,1,2,3。還有類似的加載指令,它們加載的并不是對象引用,比如iload_,lload_,fload_,和dload_, 這里i代表int,l代表long,f代表float,d代表double。局部變量的在數(shù)組中的位置大于3的,得通過iload,lload,fload,dload,和aload進行加載,這些指令都接受一個操作數(shù),它代表的是要加載的局部變量的在數(shù)組中的位置。
invokespecial
這條指令可以用來調(diào)用對象實例的構造方法,私有方法和父類中的方法。它是方法調(diào)用指令集中的一條,其它的還有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.這里的invokespecial指令調(diào)用的是父類也就是java.lang.Object的構造方法。
bipush
它是用來把一個字節(jié)作為整型壓到操作數(shù)棧中的,在這里100會被壓到操作數(shù)棧里。
putfield
它接受一個操作數(shù),這個操作數(shù)引用的是運行時常量池里的一個字段,在這里這個字段是simpleField。賦給這個字段的值,以及包含這個字段的對象引用,在執(zhí)行這條指令的時候,都 會從操作數(shù)棧頂上pop出來。前面的aload_0指令已經(jīng)把包含這個字段的對象壓到操作數(shù)棧上了,而后面的bipush又把100壓到棧里。最后putfield指令會將這兩個值從棧頂彈出。執(zhí)行完的結果就是這個對象的simpleField這個字段的值更新成了100。
上述代碼執(zhí)行的時候內(nèi)存里面是這樣的:
這里的putfield指令的操作數(shù)引用的是常量池里的第二個位置。JVM會為每個類型維護一個常量池,運行時的數(shù)據(jù)結構有點類似一個符號表,盡管它包含的信息更多。Java中的字節(jié)碼操作需要對應的數(shù)據(jù),但通常這些數(shù)據(jù)都太大了,存儲在字節(jié)碼里不適合,它們會被存儲在常量池里面,而字節(jié)碼包含一個常量池里的引用 。當類文件生成的時候,其中的一塊就是常量池:
Constant pool:
1 = Methodref #4.#16 // java/lang/Object."<init>":()V
2 = Fieldref #3.#17 // SimpleClass.simpleField:I
3 = Class #13 // SimpleClass
4 = Class #19 // java/lang/Object
5 = Utf8 simpleField
6 = Utf8 I
7 = Utf8 <init>
8 = Utf8 ()V
9 = Utf8 Code
10 = Utf8 LineNumberTable
11 = Utf8 LocalVariableTable
12 = Utf8 this
13 = Utf8 SimpleClass
14 = Utf8 SourceFile
15 = Utf8 SimpleClass.java
16 = NameAndType #7:#8 // "<init>":()V
17 = NameAndType #5:#6 // simpleField:I
18 = Utf8 LSimpleClass;
19 = Utf8 java/lang/Object
常量字段(類常量)
帶有final標記的常量字段在class文件里會被標記成ACC_FINAL.
比如
public class SimpleClass {
public final int simpleField = 100;
}
字段的描述信息會標記成ACC_FINAL:
public static final int simpleField = 100;
Signature: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 100
對應的初始化代碼并不變:
4: aload_0
5: bipush 100
7: putfield #2 // Field simpleField:I
靜態(tài)變量
帶有static修飾符的靜態(tài)變量則會被標記成ACC_STATIC:
public static int simpleField;
Signature: I
flags: ACC_PUBLIC, ACC_STATIC
不過在實例的構造方法中卻再也找不到對應的初始化代碼了。因為static變量會在類的構造方法中進行初始化,并且它用的是putstatic指令而不是putfiled。
static {};
Signature: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #2 // Field simpleField:I
5: return
未完待續(xù)。
條件語句
像if-else, switch這樣的流程控制的條件語句,是通過用一條指令來進行兩個值的比較,然后根據(jù)結果跳轉到另一條字節(jié)碼來實現(xiàn)的。
循環(huán)語句包括for循環(huán),while循環(huán),它們的實現(xiàn)方式也很類似,但有一點不同,它們通常都會包含一條goto指令,以便字節(jié)碼實現(xiàn)循環(huán)執(zhí)行。do-while循環(huán)不需要goto指令,因為它的條件分支是在字節(jié)碼的末尾。更多細節(jié)請參考循環(huán)語句一節(jié)。
有一些指令可以用來比較兩個整型或者兩個引用,然后執(zhí)行某個分支,這些操作都能在單條指令里面完成。而像double,float,long這些值需要兩條指令。首先得去比較兩個值,然后根據(jù)結果,會把1,0或者-1壓到棧里。最后根據(jù)棧頂?shù)闹凳谴笥冢扔诨蛘咝∮?來判斷應該跳轉到哪個分支。
我們先來介紹下if-else語句,然后再詳細介紹下分支跳轉用到的幾種不同的指令。
if-else
下面的這個簡單的例子是用來比較兩個整數(shù)的:
public int greaterThen(int intOne, int intTwo) {
if (intOne > intTwo) {
return 0;
} else {
return 1;
}
}
方法最后會編譯成如下的字節(jié)碼:
0: iload_1
1: iload_2
2: if_icmple 7
5: iconst_0
6: ireturn
7: iconst_1
8: ireturn
首先,通過iload_1, iload_2兩條指令將兩個入?yún)喝氩僮鲾?shù)棧中。if_icmple會比較棧頂?shù)膬蓚€值的大小。如果intOne小于或者等于intTwo的話,會跳轉到第7行處的字節(jié)碼來執(zhí)行。可以看到這里和Java代碼里的if語句的條件判斷正好相反,這是因為在字節(jié)碼里面,判斷條件為真的話會跑到else分支里面去執(zhí)行,而在Java代碼里,判斷為真會進入if塊里面執(zhí)行。換言之,if_icmple判斷的是如果if條件不為真,然后跳過if塊。if代碼塊里對應的代碼是5,6處的字節(jié)碼,而else塊對應的是7,8處的。
下面的代碼則稍微復雜了一點,它需要進行兩次比較。
public int greaterThen(float floatOne, float floatTwo) {
int result;
if (floatOne > floatTwo) {
result = 1;
} else {
result = 2;
}
return result;
}
編譯后會是這樣:
0: fload_1
1: fload_2
2: fcmpl
3: ifle 11
6: iconst_1
7: istore_3
8: goto 13
11: iconst_2
12: istore_3
13: iload_3
14: ireturn
在這個例子中,首先兩個參數(shù)會被fload_1和fload_2指令壓入棧中。和上面那個例子不同的是,這里需要比較兩回。fcmple先用來比較棧頂?shù)膄loatOne和floatTwo,然后把比較的結果壓入操作數(shù)棧中。
* floatOne > floatTwo –> 1
* floatOne = floatTwo –> 0
* floatOne < floatTwo –> -1
* floatOne or floatTwo = NaN –> 1
然后通過ifle進行判斷,如果前面fcmpl的結果是< =0的話,則跳轉到11行處的字節(jié)碼去繼續(xù)執(zhí)行。
這個例子還有一個地方和前面不同的是,它只在方法末有一個return語句,因此在if代碼塊的最后,會有一個goto語句來跳過else塊。goto語句會跳轉到第13條字節(jié)碼處,然后通過iload_3將存儲在局部變量區(qū)第三個位置的結果壓入棧中,然后就可以通過return指令將結果返回了。
除了比較數(shù)值的指令外,還有比較引用是否相等的(==),以及引用是否等于null的(== null或者!=null),以及比較對象的類型的(instanceof)。
if_icmp<cond>
這組指令用來比較操作數(shù)棧頂?shù)膬蓚€整數(shù),然后跳轉到新的位置去執(zhí)行。<cond>可以是:eq-等于,ne-不等于,lt-小于,le-小于等于,gt-大于, ge-大于等于。
if_acmp<cond>
這兩個指令用來比較對象是否相等,然后根據(jù)操作數(shù)指定的位置進行跳轉。
ifnonnull ifnull
這兩個指令用來判斷對象是否為null,然后根據(jù)操作數(shù)指定的位置進行跳轉。
lcmp
這個指令用來比較棧頂?shù)膬蓚€長整型,然后將結果值壓入棧中: 如果value1>value2,壓入1,如果value1==value2,壓入0,如果value1<value2壓入-1.
fcmp<cond> l g dcomp<cond>
這組指令用來比較兩個float或者double類型的值,然后然后將結果值壓入棧中:如果value1>value2,壓入1,如果value1==value2,壓入0,如果value1<value2壓入-1. 指令可以以l或者g結尾,不同之處在于它們是如何處理NaN的。fcmpg和dcmpg指令把整數(shù)1壓入操作數(shù)棧,而fcmpl和dcmpl把-1壓入操作數(shù)棧。這確保了比較兩個值的時候,如果其中一個不是數(shù)字(Not A Number, NaN),比較的結果不會相等。比如判斷if x > y(x和y都是浮點數(shù)),就會用的fcmpl,如果其中一個值是NaN的話,-1會被壓入棧頂,下一條指令則是ifle,如果分支小于0則跳轉。因此如果有一個是NaN的話,ifle會跳過if塊,不讓它執(zhí)行。
instanceof
如果棧頂對象的類型是指定的類的話,則將1壓入棧中。這個指令的操作數(shù)指定的是某個類型在常量池的序號。如果對象為空或者不是對應的類型,則將0壓入操作數(shù)棧中。
if<cond>
將棧頂值和0進行比較,如果條件為真,則跳轉到指定的分支繼續(xù)執(zhí)行。這些指令通常用于較復雜的條件判斷中,在一些單條指令無法完成的情況。比如驗證方法調(diào)用的返回值。
switch語句
Java switch表達式的類型只能是char,byte,short,int,Character, Byte, Short,Integer,String或者enum。JVM為了支持switch語句,用了兩個特殊的指令,叫做tableSwitch和lookupswitch,它們都只能操作整型數(shù)值。只能使用整型并不影響,因為char,byte,short和enum都可以提升成int類型。Java7開始支持String類型,下面我們會介紹到。tableswitch操作會比較快一些,不過它消耗的內(nèi)存會更多。tableswitch會列出case分支里面最大值和最小值之間的所有值,如果判斷的值不在這個范圍內(nèi)則直接跳轉到default塊執(zhí)行,case中沒有的值也會被列出,不過它們同樣指向的是default塊。拿下面的這個switch語句作為例子:
public int simpleSwitch(int intOne) {
switch (intOne) {
case 0:
return 3;
case 1:
return 2;
case 4:
return 1;
default:
return -1;
}
}
編譯后會生成如下的字節(jié)碼
0: iload_1
1: tableswitch {
default: 42
min: 0
max: 4
0: 36
1: 38
2: 42
3: 42
4: 40
}
36: iconst_3
37: ireturn
38: iconst_2
39: ireturn
40: iconst_1
41: ireturn
42: iconst_m1
43: ireturn
tableswitch指令里0,1,4的值和代碼里的case語句一一對應,它們指向的是對應代碼塊的字節(jié)碼。tableswitch指令同樣有2,3的值,但代碼中并沒有對應的case語句,它們指向的是default代碼塊。當這條指令執(zhí)行的時候,會判斷操作數(shù)棧頂?shù)闹凳欠裨谧畲笾岛妥钚≈抵g。如果不在的話,直接跳去default分支,也就是上面的42行處的字節(jié)碼。為了確保能找到default分支,它都是出現(xiàn)在tableswitch指令的第一個字節(jié)(如果需要內(nèi)存對齊的話,則在補齊了之后的第一個字節(jié))。如果棧頂?shù)闹翟谧畲笞钚≈档姆秶鷥?nèi),則用它作為tableswtich內(nèi)部的索引,定位到應該跳轉的分支。比如1的話,就會跳轉至38行處繼續(xù)執(zhí)行。下圖會演示這條指令是如何執(zhí)行的:
如果case語句里面的值取值范圍太廣了(也就是太分散了)這個方法就不太好了,因為它占用的內(nèi)存太多了。因此當switch的case條件里面的值比較分散的時候,就會使用lookupswitch指令。這個指令會列出case語句里的所有跳轉的分支,但它沒有列出所有可能的值。當執(zhí)行這條指令的時候,棧頂?shù)闹禃蚻ookupswitch里的每個值進行比較,來確定要跳轉的分支。執(zhí)行l(wèi)ookupswitch指令的時候,JVM會在列表中查找匹配的元素,這和tableswitch比起來要慢一些,因為tableswitch直接用索引就定位到正確的位置了。當switch語句編譯的時候,編譯器必須去權衡內(nèi)存的使用和性能的影響,來決定到底該使用哪條指令。下面的代碼,編譯器會生成lookupswitch語句:
public int simpleSwitch(int intOne) {
switch (intOne) {
case 10:
return 1;
case 20:
return 2;
case 30:
return 3;
default:
return -1;
}
}
生成后的字節(jié)碼如下:
0: iload_1
1: lookupswitch {
default: 42
count: 3
10: 36
20: 38
30: 40
}
36: iconst_1
37: ireturn
38: iconst_2
39: ireturn
40: iconst_3
41: ireturn
42: iconst_m1
43: ireturn
為了確保搜索算法的高效(得比線性查找要快),這里會提供列表的長度,同時匹配的元素也是排好序的。下圖演示了lookupswitch指令是如何執(zhí)行的。
從Java7開始,switch語句增加了對String類型的支持。不過字節(jié)碼中的switch指令還是只支持int類型,并沒有增加對其它類型的支持。事實上switch語句對String的支持是分成兩個步驟來完成的。首先,將每個case語句里的值的hashCode和操作數(shù)棧頂?shù)闹担ㄗg注:也就是switch里面的那個值,這個值會先壓入棧頂)進行比較。這個可以通過lookupswitch或者是tableswitch指令來完成。結果會路由到某個分支上,然后調(diào)用String.equlals來判斷是否確實匹配。最后根據(jù)equals返回的結果,再用一個tableswitch指令來路由到具體的case分支上去執(zhí)行。
public int simpleSwitch(String stringOne) {
switch (stringOne) {
case "a":
return 0;
case "b":
return 2;
case "c":
return 3;
default:
return 4;
}
}
這個字符串的switch語句會生成下面的字節(jié)碼:
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
8: tableswitch {
default: 75
min: 97
max: 99
97: 36
98: 50
99: 64
}
36: aload_2
37: ldc #3 // String a
39: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42: ifeq 75
45: iconst_0
46: istore_3
47: goto 75
50: aload_2
51: ldc #5 // String b
53: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 75
59: iconst_1
60: istore_3
61: goto 75
64: aload_2
65: ldc #6 // String c
67: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
70: ifeq 75
73: iconst_2
74: istore_3
75: iload_3
76: tableswitch {
default: 110
min: 0
max: 2
0: 104
1: 106
2: 108
}
104: iconst_0
105: ireturn
106: iconst_2
107: ireturn
108: iconst_3
109: ireturn
110: iconst_4
111: ireturn
這段字節(jié)碼所在的class文件里面,會包含如下的一個常量池。關于常量池可以看下JVM內(nèi)部細節(jié)中的運行時常量池一節(jié)。
Constant pool:
2 = Methodref #25.#26 // java/lang/String.hashCode:()I
3 = String #27 // a
4 = Methodref #25.#28 // java/lang/String.equals:(Ljava/lang/Object;)Z
5 = String #29 // b
6 = String #30 // c
25 = Class #33 // java/lang/String
26 = NameAndType #34:#35 // hashCode:()I
27 = Utf8 a
28 = NameAndType #36:#37 // equals:(Ljava/lang/Object;)Z
29 = Utf8 b
30 = Utf8 c
33 = Utf8 java/lang/String
34 = Utf8 hashCode
35 = Utf8 ()I
36 = Utf8 equals
37 = Utf8 (Ljava/lang/Object;)Z
注意,在執(zhí)行這個switch語句的時候,用到了兩個tableswitch指令,同時還有數(shù)個invokevirtual指令,這個是用來調(diào)用String.equals()方法的。在下一篇文章中關于方法調(diào)用的那節(jié),會詳細介紹到這個invokevirtual指令。下圖演示了輸入為”b”的情況下,這個swith語句是如何執(zhí)行的。
如果有幾個分支的hashcode是一樣的話,比如說“FB”和”Ea”,它們的hashCode都是28,得簡單的調(diào)整下equals方法的處理流程來進行處理。在下面的這個例子中,34行處的字節(jié)碼ifeg 42會跳轉到另一個String.equals方法調(diào)用,而不是像前面那樣執(zhí)行l(wèi)ookupswitch指令,因為前面的那個例子中hashCode沒有沖突。(譯注:這里一般容易弄混淆,認為ifeq是字符串相等,為什么要跳到下一處繼續(xù)比較字符串?其實ifeq是判斷棧頂元素是否和0相等,而棧頂?shù)闹稻褪荢tring.equals的返回值,而true,也就是相等,返回的是1,false返回的是0,因此ifeq為真的時候表明返回的是false,這會兒就應該繼續(xù)進行下一個字符串的比較)
public int simpleSwitch(String stringOne) {
switch (stringOne) {
case "FB":
return 0;
case "Ea":
return 2;
default:
return 4;
}
}
這段代碼會生成下面的字節(jié)碼:
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
8: lookupswitch {
default: 53
count: 1
2236: 28
}
28: aload_2
29: ldc #3 // String Ea
31: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq 42
37: iconst_1
38: istore_3
39: goto 53
42: aload_2
43: ldc #5 // String FB
45: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
48: ifeq 53
51: iconst_0
52: istore_3
53: iload_3
54: lookupswitch {
default: 84
count: 2
0: 80
1: 82
}
80: iconst_0
81: ireturn
82: iconst_2
83: ireturn
84: iconst_4
85: ireturn
循環(huán)語句
if-else和switch這些條件流程控制語句都是先通過一條指令比較兩個值,然后跳轉到某個分支去執(zhí)行。
for循環(huán)和while循環(huán)這些語句也類似,只不過它們通常都包含一個goto指令,使得字節(jié)碼能夠循環(huán)執(zhí)行。do-while循環(huán)則不需要goto指令,因為它們的條件判斷指令是放在循環(huán)體的最后來執(zhí)行。
有一些操作碼能在單條指令內(nèi)完成整數(shù)或者引用的比較,然后根據(jù)結果跳轉到某個分支繼續(xù)執(zhí)行。而比較double,long,float這些類型則需要兩條指令。首先會將兩個值進行比較,然后根據(jù)結果把1,-1,0壓入操作數(shù)棧中。然后再根據(jù)棧頂?shù)闹凳谴笥谛∮诨蛘叩扔?,來決定下一步要執(zhí)行的指令的位置。這些指令在上一篇文章中有詳細的介紹。
while循環(huán)
while循環(huán)包含條件跳轉指令比如if_icmpge 或者if_icmplt(前面有介紹)以及goto指令。如果判斷條件不滿足的話,會跳轉到循環(huán)體后的第一條指令繼續(xù)執(zhí)行,循環(huán)結束(譯注:這里判斷條件和代碼中的正好相反,如代碼中是i<2,字節(jié)碼內(nèi)是i>=2,從字節(jié)碼的角度看,是滿足條件后循環(huán)中止)。循環(huán)體的末尾是一條goto指令,它會跳轉到循環(huán)開始的地方繼續(xù)執(zhí)行,直到分支跳轉的條件滿足才終止。
public void whileLoop() {
int i = 0;
while (i < 2) {
i++;
}
}
編譯完后是:
0: iconst_0
1: istore_1
2: iload_1
3: iconst_2
4: if_icmpge 13
7: iinc 1, 1
10: goto 2
13: return
if_icmpge指令會判斷局部變量區(qū)中的1號位的變量(也就是i,譯注:局部變量區(qū)從0開始計數(shù),第0位是this)是否大于等于2,如果不是繼續(xù)執(zhí)行,如果是的話跳轉到13行處,結束循環(huán)。goto指令使得循環(huán)可以繼續(xù)執(zhí)行,直到條件判斷為真,這個時候會跳轉到緊挨著循環(huán)體后邊的return指令處。iinc是少數(shù)的幾條能直接更新局部變量區(qū)里的變量的指令之一,它不用把值壓到操作數(shù)棧里面就能直接進行操作。這里iinc指令把第1個局部變量(譯注:第0個是this)自增1。
for循環(huán)和while循環(huán)在字節(jié)碼里的格式是一樣的。這并不奇怪,因為每個while循環(huán)都可以很容易改寫成一個for循環(huán)。比如上面的while循環(huán)就可以改寫成下面的for循環(huán),當然了它們輸出的字節(jié)碼也是一樣的:
public void forLoop() {
for(int i = 0; i < 2; i++) {
}
}
do-while循環(huán)
do-while循環(huán)和for循環(huán),while循環(huán)非常類似,除了一點,它是不需要goto指令的,因為條件跳轉指令在循環(huán)體的末尾,可以用它來跳轉回循環(huán)體的起始處。
public void doWhileLoop() {
int i = 0;
do {
i++;
} while (i < 2);
}
這會生成如下的字節(jié)碼:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: iconst_2
7: if_icmplt 2
10: return
本文最早發(fā)表于本人博客:http://it.deepinmind.com
指令碼 助記符 說明
0x00 nop 什么都不做
0x01 aconst_null 將null推送至棧頂
0x02 iconst_m1 將int型-1推送至棧頂
0x03 iconst_0 將int型0推送至棧頂
0x04 iconst_1 將int型1推送至棧頂
0x05 iconst_2 將int型2推送至棧頂
0x06 iconst_3 將int型3推送至棧頂
0x07 iconst_4 將int型4推送至棧頂
0x08 iconst_5 將int型5推送至棧頂
0x09 lconst_0 將long型0推送至棧頂
0x0a lconst_1 將long型1推送至棧頂
0x0b fconst_0 將float型0推送至棧頂
0x0c fconst_1 將float型1推送至棧頂
0x0d fconst_2 將float型2推送至棧頂
0x0e dconst_0 將double型0推送至棧頂
0x0f dconst_1 將double型1推送至棧頂
0x10 bipush 將單字節(jié)的常量值(-128~127)推送至棧頂
0x11 sipush 將一個短整型常量值(-32768~32767)推送至棧頂
0x12 ldc 將int, float或String型常量值從常量池中推送至棧頂
0x13 ldc_w 將int, float或String型常量值從常量池中推送至棧頂(寬索引)
0x14 ldc2_w 將long或double型常量值從常量池中推送至棧頂(寬索引)
0x15 iload 將指定的int型本地變量推送至棧頂
0x16 lload 將指定的long型本地變量推送至棧頂
0x17 fload 將指定的float型本地變量推送至棧頂
0x18 dload 將指定的double型本地變量推送至棧頂
0x19 aload 將指定的引用類型本地變量推送至棧頂
0x1a iload_0 將第一個int型本地變量推送至棧頂
0x1b iload_1 將第二個int型本地變量推送至棧頂
0x1c iload_2 將第三個int型本地變量推送至棧頂
0x1d iload_3 將第四個int型本地變量推送至棧頂
0x1e lload_0 將第一個long型本地變量推送至棧頂
0x1f lload_1 將第二個long型本地變量推送至棧頂
0x20 lload_2 將第三個long型本地變量推送至棧頂
0x21 lload_3 將第四個long型本地變量推送至棧頂
0x22 fload_0 將第一個float型本地變量推送至棧頂
0x23 fload_1 將第二個float型本地變量推送至棧頂
0x24 fload_2 將第三個float型本地變量推送至棧頂
0x25 fload_3 將第四個float型本地變量推送至棧頂
0x26 dload_0 將第一個double型本地變量推送至棧頂
0x27 dload_1 將第二個double型本地變量推送至棧頂
0x28 dload_2 將第三個double型本地變量推送至棧頂
0x29 dload_3 將第四個double型本地變量推送至棧頂
0x2a aload_0 將第一個引用類型本地變量推送至棧頂
0x2b aload_1 將第二個引用類型本地變量推送至棧頂
0x2c aload_2 將第三個引用類型本地變量推送至棧頂
0x2d aload_3 將第四個引用類型本地變量推送至棧頂
0x2e iaload 將int型數(shù)組指定索引的值推送至棧頂
0x2f laload 將long型數(shù)組指定索引的值推送至棧頂
0x30 faload 將float型數(shù)組指定索引的值推送至棧頂
0x31 daload 將double型數(shù)組指定索引的值推送至棧頂
0x32 aaload 將引用型數(shù)組指定索引的值推送至棧頂
0x33 baload 將boolean或byte型數(shù)組指定索引的值推送至棧頂
0x34 caload 將char型數(shù)組指定索引的值推送至棧頂
0x35 saload 將short型數(shù)組指定索引的值推送至棧頂
0x36 istore 將棧頂int型數(shù)值存入指定本地變量
0x37 lstore 將棧頂long型數(shù)值存入指定本地變量
0x38 fstore 將棧頂float型數(shù)值存入指定本地變量
0x39 dstore 將棧頂double型數(shù)值存入指定本地變量
0x3a astore 將棧頂引用型數(shù)值存入指定本地變量
0x3b istore_0 將棧頂int型數(shù)值存入第一個本地變量
0x3c istore_1 將棧頂int型數(shù)值存入第二個本地變量
0x3d istore_2 將棧頂int型數(shù)值存入第三個本地變量
0x3e istore_3 將棧頂int型數(shù)值存入第四個本地變量
0x3f lstore_0 將棧頂long型數(shù)值存入第一個本地變量
0x40 lstore_1 將棧頂long型數(shù)值存入第二個本地變量
0x41 lstore_2 將棧頂long型數(shù)值存入第三個本地變量
0x42 lstore_3 將棧頂long型數(shù)值存入第四個本地變量
0x43 fstore_0 將棧頂float型數(shù)值存入第一個本地變量
0x44 fstore_1 將棧頂float型數(shù)值存入第二個本地變量
0x45 fstore_2 將棧頂float型數(shù)值存入第三個本地變量
0x46 fstore_3 將棧頂float型數(shù)值存入第四個本地變量
0x47 dstore_0 將棧頂double型數(shù)值存入第一個本地變量
0x48 dstore_1 將棧頂double型數(shù)值存入第二個本地變量
0x49 dstore_2 將棧頂double型數(shù)值存入第三個本地變量
0x4a dstore_3 將棧頂double型數(shù)值存入第四個本地變量
0x4b astore_0 將棧頂引用型數(shù)值存入第一個本地變量
0x4c astore_1 將棧頂引用型數(shù)值存入第二個本地變量
0x4d astore_2 將棧頂引用型數(shù)值存入第三個本地變量
0x4e astore_3 將棧頂引用型數(shù)值存入第四個本地變量
0x4f iastore 將棧頂int型數(shù)值存入指定數(shù)組的指定索引位置
0x50 lastore 將棧頂long型數(shù)值存入指定數(shù)組的指定索引位置
0x51 fastore 將棧頂float型數(shù)值存入指定數(shù)組的指定索引位置
0x52 dastore 將棧頂double型數(shù)值存入指定數(shù)組的指定索引位置
0x53 aastore 將棧頂引用型數(shù)值存入指定數(shù)組的指定索引位置
0x54 bastore 將棧頂boolean或byte型數(shù)值存入指定數(shù)組的指定索引位置
0x55 castore 將棧頂char型數(shù)值存入指定數(shù)組的指定索引位置
0x56 sastore 將棧頂short型數(shù)值存入指定數(shù)組的指定索引位置
0x57 pop 將棧頂數(shù)值彈出 (數(shù)值不能是long或double類型的)
0x58 pop2 將棧頂?shù)囊粋€(long或double類型的)或兩個數(shù)值彈出(其它)
0x59 dup 復制棧頂數(shù)值并將復制值壓入棧頂
0x5a dup_x1 復制棧頂數(shù)值并將兩個復制值壓入棧頂
0x5b dup_x2 復制棧頂數(shù)值并將三個(或兩個)復制值壓入棧頂
0x5c dup2 復制棧頂一個(long或double類型的)或兩個(其它)數(shù)值并將復制值壓入棧頂
0x5d dup2_x1 <待補充>
0x5e dup2_x2 <待補充>
0x5f swap 將棧最頂端的兩個數(shù)值互換(數(shù)值不能是long或double類型的)
0x60 iadd 將棧頂兩int型數(shù)值相加并將結果壓入棧頂
0x61 ladd 將棧頂兩long型數(shù)值相加并將結果壓入棧頂
0x62 fadd 將棧頂兩float型數(shù)值相加并將結果壓入棧頂
0x63 dadd 將棧頂兩double型數(shù)值相加并將結果壓入棧頂
0x64 isub 將棧頂兩int型數(shù)值相減并將結果壓入棧頂
0x65 lsub 將棧頂兩long型數(shù)值相減并將結果壓入棧頂
0x66 fsub 將棧頂兩float型數(shù)值相減并將結果壓入棧頂
0x67 dsub 將棧頂兩double型數(shù)值相減并將結果壓入棧頂
0x68 imul 將棧頂兩int型數(shù)值相乘并將結果壓入棧頂
0x69 lmul 將棧頂兩long型數(shù)值相乘并將結果壓入棧頂
0x6a fmul 將棧頂兩float型數(shù)值相乘并將結果壓入棧頂
0x6b dmul 將棧頂兩double型數(shù)值相乘并將結果壓入棧頂
0x6c idiv 將棧頂兩int型數(shù)值相除并將結果壓入棧頂
0x6d ldiv 將棧頂兩long型數(shù)值相除并將結果壓入棧頂
0x6e fdiv 將棧頂兩float型數(shù)值相除并將結果壓入棧頂
0x6f ddiv 將棧頂兩double型數(shù)值相除并將結果壓入棧頂
0x70 irem 將棧頂兩int型數(shù)值作取模運算并將結果壓入棧頂
0x71 lrem 將棧頂兩long型數(shù)值作取模運算并將結果壓入棧頂
0x72 frem 將棧頂兩float型數(shù)值作取模運算并將結果壓入棧頂
0x73 drem 將棧頂兩double型數(shù)值作取模運算并將結果壓入棧頂
0x74 ineg 將棧頂int型數(shù)值取負并將結果壓入棧頂
0x75 lneg 將棧頂long型數(shù)值取負并將結果壓入棧頂
0x76 fneg 將棧頂float型數(shù)值取負并將結果壓入棧頂
0x77 dneg 將棧頂double型數(shù)值取負并將結果壓入棧頂
0x78 ishl 將int型數(shù)值左移位指定位數(shù)并將結果壓入棧頂
0x79 lshl 將long型數(shù)值左移位指定位數(shù)并將結果壓入棧頂
0x7a ishr 將int型數(shù)值右(符號)移位指定位數(shù)并將結果壓入棧頂
0x7b lshr 將long型數(shù)值右(符號)移位指定位數(shù)并將結果壓入棧頂
0x7c iushr 將int型數(shù)值右(無符號)移位指定位數(shù)并將結果壓入棧頂
0x7d lushr 將long型數(shù)值右(無符號)移位指定位數(shù)并將結果壓入棧頂
0x7e iand 將棧頂兩int型數(shù)值作“按位與”并將結果壓入棧頂
0x7f land 將棧頂兩long型數(shù)值作“按位與”并將結果壓入棧頂
0x80 ior 將棧頂兩int型數(shù)值作“按位或”并將結果壓入棧頂
0x81 lor 將棧頂兩long型數(shù)值作“按位或”并將結果壓入棧頂
0x82 ixor 將棧頂兩int型數(shù)值作“按位異或”并將結果壓入棧頂
0x83 lxor 將棧頂兩long型數(shù)值作“按位異或”并將結果壓入棧頂
0x84 iinc 將指定int型變量增加指定值(i++, i--, i+=2)
0x85 i2l 將棧頂int型數(shù)值強制轉換成long型數(shù)值并將結果壓入棧頂
0x86 i2f 將棧頂int型數(shù)值強制轉換成float型數(shù)值并將結果壓入棧頂
0x87 i2d 將棧頂int型數(shù)值強制轉換成double型數(shù)值并將結果壓入棧頂
0x88 l2i 將棧頂long型數(shù)值強制轉換成int型數(shù)值并將結果壓入棧頂
0x89 l2f 將棧頂long型數(shù)值強制轉換成float型數(shù)值并將結果壓入棧頂
0x8a l2d 將棧頂long型數(shù)值強制轉換成double型數(shù)值并將結果壓入棧頂
0x8b f2i 將棧頂float型數(shù)值強制轉換成int型數(shù)值并將結果壓入棧頂
0x8c f2l 將棧頂float型數(shù)值強制轉換成long型數(shù)值并將結果壓入棧頂
0x8d f2d 將棧頂float型數(shù)值強制轉換成double型數(shù)值并將結果壓入棧頂
0x8e d2i 將棧頂double型數(shù)值強制轉換成int型數(shù)值并將結果壓入棧頂
0x8f d2l 將棧頂double型數(shù)值強制轉換成long型數(shù)值并將結果壓入棧頂
0x90 d2f 將棧頂double型數(shù)值強制轉換成float型數(shù)值并將結果壓入棧頂
0x91 i2b 將棧頂int型數(shù)值強制轉換成byte型數(shù)值并將結果壓入棧頂
0x92 i2c 將棧頂int型數(shù)值強制轉換成char型數(shù)值并將結果壓入棧頂
0x93 i2s 將棧頂int型數(shù)值強制轉換成short型數(shù)值并將結果壓入棧頂
0x94 lcmp 比較棧頂兩long型數(shù)值大小,并將結果(1,0,-1)壓入棧頂
0x95 fcmpl 比較棧頂兩float型數(shù)值大小,并將結果(1,0,-1)壓入棧頂;當其中一個數(shù)值為NaN時,將-1壓入棧頂
0x96 fcmpg 比較棧頂兩float型數(shù)值大小,并將結果(1,0,-1)壓入棧頂;當其中一個數(shù)值為NaN時,將1壓入棧頂
0x97 dcmpl 比較棧頂兩double型數(shù)值大小,并將結果(1,0,-1)壓入棧頂;當其中一個數(shù)值為NaN時,將-1壓入棧頂
0x98 dcmpg 比較棧頂兩double型數(shù)值大小,并將結果(1,0,-1)壓入棧頂;當其中一個數(shù)值為NaN時,將1壓入棧頂
0x99 ifeq 當棧頂int型數(shù)值等于0時跳轉
0x9a ifne 當棧頂int型數(shù)值不等于0時跳轉
0x9b iflt 當棧頂int型數(shù)值小于0時跳轉
0x9c ifge 當棧頂int型數(shù)值大于等于0時跳轉
0x9d ifgt 當棧頂int型數(shù)值大于0時跳轉
0x9e ifle 當棧頂int型數(shù)值小于等于0時跳轉
0x9f if_icmpeq 比較棧頂兩int型數(shù)值大小,當結果等于0時跳轉
0xa0 if_icmpne 比較棧頂兩int型數(shù)值大小,當結果不等于0時跳轉
0xa1 if_icmplt 比較棧頂兩int型數(shù)值大小,當結果小于0時跳轉
0xa2 if_icmpge 比較棧頂兩int型數(shù)值大小,當結果大于等于0時跳轉
0xa3 if_icmpgt 比較棧頂兩int型數(shù)值大小,當結果大于0時跳轉
0xa4 if_icmple 比較棧頂兩int型數(shù)值大小,當結果小于等于0時跳轉
0xa5 if_acmpeq 比較棧頂兩引用型數(shù)值,當結果相等時跳轉
0xa6 if_acmpne 比較棧頂兩引用型數(shù)值,當結果不相等時跳轉
0xa7 goto 無條件跳轉
0xa8 jsr 跳轉至指定16位offset位置,并將jsr下一條指令地址壓入棧頂
0xa9 ret 返回至本地變量指定的index的指令位置(一般與jsr, jsr_w聯(lián)合使用)
0xaa tableswitch 用于switch條件跳轉,case值連續(xù)(可變長度指令)
0xab lookupswitch 用于switch條件跳轉,case值不連續(xù)(可變長度指令)
0xac ireturn 從當前方法返回int
0xad lreturn 從當前方法返回long
0xae freturn 從當前方法返回float
0xaf dreturn 從當前方法返回double
0xb0 areturn 從當前方法返回對象引用
0xb1 return 從當前方法返回void
0xb2 getstatic 獲取指定類的靜態(tài)域,并將其值壓入棧頂
0xb3 putstatic 為指定的類的靜態(tài)域賦值
0xb4 getfield 獲取指定類的實例域,并將其值壓入棧頂
0xb5 putfield 為指定的類的實例域賦值
0xb6 invokevirtual 調(diào)用實例方法
0xb7 invokespecial 調(diào)用超類構造方法,實例初始化方法,私有方法
0xb8 invokestatic 調(diào)用靜態(tài)方法
0xb9 invokeinterface 調(diào)用接口方法
0xba --
0xbb new 創(chuàng)建一個對象,并將其引用值壓入棧頂
0xbc newarray 創(chuàng)建一個指定原始類型(如int, float, char…)的數(shù)組,并將其引用值壓入棧頂
0xbd anewarray 創(chuàng)建一個引用型(如類,接口,數(shù)組)的數(shù)組,并將其引用值壓入棧頂
0xbe arraylength 獲得數(shù)組的長度值并壓入棧頂
0xbf athrow 將棧頂?shù)漠惓伋?/p>
0xc0 checkcast 檢驗類型轉換,檢驗未通過將拋出ClassCastException
0xc1 instanceof 檢驗對象是否是指定的類的實例,如果是將1壓入棧頂,否則將0壓入棧頂
0xc2 monitorenter 獲得對象的鎖,用于同步方法或同步塊
0xc3 monitorexit 釋放對象的鎖,用于同步方法或同步塊
0xc4 wide <待補充>
0xc5 multianewarray 創(chuàng)建指定類型和指定維度的多維數(shù)組(執(zhí)行該指令時,操作棧中必須包含各維度的長度值),并將其引用值壓入棧頂
0xc6 ifnull 為null時跳轉
0xc7 ifnonnull 不為null時跳轉
0xc8 goto_w 無條件跳轉(寬索引)
0xc9 jsr_w 跳轉至指定32位offset位置,并將jsr_w下一條指令地址壓入棧頂