Java字节码浅析(三)
英文原文链接,译文链接,原文作者:James Bloom,译者:有孚
从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 |