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