Java字節碼淺析(三)
從Java7開始,switch語句增加了對String類型的支持。不過字節碼中的switch指令還是隻支持int類型,並沒有增加對其它類型的支持。事實上switch語句對String的支持是分成兩個步驟來完成的。首先,將每個case語句裏的值的hashCode和操作數棧頂的值(譯注:也就是switch裏麵的那個值,這個值會先壓入棧頂)進行比較。這個可以通過lookupswitch或者是tableswitch指令來完成。結果會路由到某個分支上,然後調用String.equlals來判斷是否確實匹配。最後根據equals返回的結果,再用一個tableswitch指令來路由到具體的case分支上去執行。
01 |
public int simpleSwitch(String stringOne) {
|
02 |
switch (stringOne) {
|
03 |
case "a" :
|
04 |
return 0 ;
|
05 |
case "b" :
|
06 |
return 2 ;
|
07 |
case "c" :
|
08 |
return 3 ;
|
09 |
default :
|
10 |
return 4 ;
|
11 |
}
|
12 |
} |
這個字符串的switch語句會生成下麵的字節碼:
01 |
0 : aload_1
|
02 |
1 : astore_2
|
03 |
2 : iconst_m1
|
04 |
3 : istore_3
|
05 |
4 : aload_2
|
06 |
5 : invokevirtual # 2 // Method java/lang/String.hashCode:()I
|
07 |
8 : tableswitch {
|
08 |
default : 75
|
09 |
min: 97
|
10 |
max: 99
|
11 |
97 : 36
|
12 |
98 : 50
|
13 |
99 : 64
|
14 |
}
|
15 |
36 : aload_2
|
16 |
37 : ldc # 3 // String a
|
17 |
39 : invokevirtual # 4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
18 |
42 : ifeq 75
|
19 |
45 : iconst_0
|
20 |
46 : istore_3
|
21 |
47 : goto 75
|
22 |
50 : aload_2
|
23 |
51 : ldc # 5 // String b
|
24 |
53 : invokevirtual # 4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
25 |
56 : ifeq 75
|
26 |
59 : iconst_1
|
27 |
60 : istore_3
|
28 |
61 : goto 75
|
29 |
64 : aload_2
|
30 |
65 : ldc # 6 // String c
|
31 |
67 : invokevirtual # 4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
32 |
70 : ifeq 75
|
33 |
73 : iconst_2
|
34 |
74 : istore_3
|
35 |
75 : iload_3
|
36 |
76 : tableswitch {
|
37 |
default : 110
|
38 |
min: 0
|
39 |
max: 2
|
40 |
0 : 104
|
41 |
1 : 106
|
42 |
2 : 108
|
43 |
}
|
44 |
104 : iconst_0
|
45 |
105 : ireturn
|
46 |
106 : iconst_2
|
47 |
107 : ireturn
|
48 |
108 : iconst_3
|
49 |
109 : ireturn
|
50 |
110 : iconst_4
|
51 |
111 : ireturn
|
這段字節碼所在的class文件裏麵,會包含如下的一個常量池。關於常量池可以看下JVM內部細節中的_運行時常量池_一節。
01 |
Constant pool: |
02 |
# 2 = Methodref # 25 .# 26 // java/lang/String.hashCode:()I
|
03 |
# 3 = String # 27 // a
|
04 |
# 4 = Methodref # 25 .# 28 // java/lang/String.equals:(Ljava/lang/Object;)Z
|
05 |
# 5 = String # 29 // b
|
06 |
# 6 = String # 30 // c
|
07 |
08 |
# 25 = Class # 33 // java/lang/String
|
09 |
# 26 = NameAndType # 34 :# 35 // hashCode:()I
|
10 |
# 27 = Utf8 a
|
11 |
# 28 = NameAndType # 36 :# 37 // equals:(Ljava/lang/Object;)Z
|
12 |
# 29 = Utf8 b
|
13 |
# 30 = Utf8 c
|
14 |
15 |
# 33 = Utf8 java/lang/String
|
16 |
# 34 = Utf8 hashCode
|
17 |
# 35 = Utf8 ()I
|
18 |
# 36 = Utf8 equals
|
19 |
# 37 = Utf8 (Ljava/lang/Object;)Z
|
注意,在執行這個switch語句的時候,用到了兩個tableswitch指令,同時還有數個invokevirtual指令,這個是用來調用String.equals()方法的。在下一篇文章中關於方法調用的那節,會詳細介紹到這個invokevirtual指令。下圖演示了輸入為”b”的情況下,這個swith語句是如何執行的。
如果有幾個分支的hashcode是一樣的話,比如說“FB”和”Ea”,它們的hashCode都是28,得簡單的調整下equals方法的處理流程來進行處理。在下麵的這個例子中,34行處的字節碼ifeg 42會跳轉到另一個String.equals方法調用,而不是像前麵那樣執行lookupswitch指令,因為前麵的那個例子中hashCode沒有衝突。(譯注:這裏一般容易弄混淆,認為ifeq是字符串相等,為什麼要跳到下一處繼續比較字符串?其實ifeq是判斷棧頂元素是否和0相等,而棧頂的值就是String.equals的返回值,而true,也就是相等,返回的是1,false返回的是0,因此ifeq為真的時候表明返回的是false,這會兒就應該繼續進行下一個字符串的比較)
01 |
public int simpleSwitch(String stringOne) {
|
02 |
switch (stringOne) {
|
03 |
case "FB" :
|
04 |
return 0 ;
|
05 |
case "Ea" :
|
06 |
return 2 ;
|
07 |
default :
|
08 |
return 4 ;
|
09 |
}
|
10 |
} |
這段代碼會生成下麵的字節碼:
01 |
0 : aload_1
|
02 |
1 : astore_2
|
03 |
2 : iconst_m1
|
04 |
3 : istore_3
|
05 |
4 : aload_2
|
06 |
5 : invokevirtual # 2 // Method java/lang/String.hashCode:()I
|
07 |
8 : lookupswitch {
|
08 |
default : 53
|
09 |
count: 1
|
10 |
2236 : 28
|
11 |
}
|
12 |
28 : aload_2
|
13 |
29 : ldc # 3 // String Ea
|
14 |
31 : invokevirtual # 4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
15 |
34 : ifeq 42
|
16 |
37 : iconst_1
|
17 |
38 : istore_3
|
18 |
39 : goto 53
|
19 |
42 : aload_2
|
20 |
43 : ldc # 5 // String FB
|
21 |
45 : invokevirtual # 4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
|
22 |
48 : ifeq 53
|
23 |
51 : iconst_0
|
24 |
52 : istore_3
|
25 |
53 : iload_3
|
26 |
54 : lookupswitch {
|
27 |
default : 84
|
28 |
count: 2
|
29 |
0 : 80
|
30 |
1 : 82
|
31 |
}
|
32 |
80 : iconst_0
|
33 |
81 : ireturn
|
34 |
82 : iconst_2
|
35 |
83 : ireturn
|
36 |
84 : iconst_4
|
37 |
85 : ireturn
|
###循環語句
if-else和switch這些條件流程控製語句都是先通過一條指令比較兩個值,然後跳轉到某個分支去執行。
for循環和while循環這些語句也類似,隻不過它們通常都包含一個goto指令,使得字節碼能夠循環執行。do-while循環則不需要goto指令,因為它們的條件判斷指令是放在循環體的最後來執行。
有一些操作碼能在單條指令內完成整數或者引用的比較,然後根據結果跳轉到某個分支繼續執行。而比較double,long,float這些類型則需要兩條指令。首先會將兩個值進行比較,然後根據結果把1,-1,0壓入操作數棧中。然後再根據棧頂的值是大於小於或者等於0,來決定下一步要執行的指令的位置。這些指令在上一篇文章中有詳細的介紹。
####while循環
while循環包含條件跳轉指令比如if_icmpge 或者if_icmplt(前麵有介紹)以及goto指令。如果判斷條件不滿足的話,會跳轉到循環體後的第一條指令繼續執行,循環結束(譯注:這裏判斷條件和代碼中的正好相反,如代碼中是i<2,字節碼內是i>=2,從字節碼的角度看,是滿足條件後循環中止)。循環體的末尾是一條goto指令,它會跳轉到循環開始的地方繼續執行,直到分支跳轉的條件滿足才終止。
1 |
public void whileLoop() {
|
2 |
int i = 0 ;
|
3 |
while (i < 2 ) {
|
4 |
i++;
|
5 |
}
|
6 |
} |
編譯完後是:
1 |
0 : iconst_0
|
2 |
1 : istore_1
|
3 |
2 : iload_1
|
4 |
3 : iconst_2
|
5 |
4 : if_icmpge 13
|
6 |
7 : iinc 1 , 1
|
7 |
10 : goto 2
|
8 |
13 : return
|
if_icmpge指令會判斷局部變量區中的1號位的變量(也就是i,譯注:局部變量區從0開始計數,第0位是this)是否大於等於2,如果不是繼續執行,如果是的話跳轉到13行處,結束循環。goto指令使得循環可以繼續執行,直到條件判斷為真,這個時候會跳轉到緊挨著循環體後邊的return指令處。iinc是少數的幾條能直接更新局部變量區裏的變量的指令之一,它不用把值壓到操作數棧裏麵就能直接進行操作。這裏iinc指令把第1個局部變量(譯注:第0個是this)自增1。
for循環和while循環在字節碼裏的格式是一樣的。這並不奇怪,因為每個while循環都可以很容易改寫成一個for循環。比如上麵的while循環就可以改寫成下麵的for循環,當然了它們輸出的字節碼也是一樣的:
1 |
public void forLoop() {
|
2 |
for ( int i = 0 ; i < 2 ; i++) {
|
3 |
4 |
}
|
5 |
} |
####do-while循環
do-while循環和for循環,while循環非常類似,除了一點,它是不需要goto指令的,因為條件跳轉指令在循環體的末尾,可以用它來跳轉回循環體的起始處。
1 |
public void doWhileLoop() {
|
2 |
int i = 0 ;
|
3 |
do {
|
4 |
i++;
|
5 |
} while (i < 2 );
|
6 |
} |
這會生成如下的字節碼:
1 |
0 : iconst_0
|
2 |
1 : istore_1
|
3 |
2 : iinc 1 , 1
|
4 |
5 : iload_1
|
5 |
6 : iconst_2
|
6 |
7 : if_icmplt 2
|
7 |
10 : return
|
最後更新:2017-05-23 18:02:44