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          default75
09              min: 97
10              max: 99
11               9736
12               9850
13               9964
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 47goto          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 61goto          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          default110
38              min: 0
39              max: 2
40                0104
41                1106
42                2108
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          default53
09            count: 1
10             223628
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 39goto          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          default84
28            count: 2
29                080
30                182
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 11
7 10goto 2
8 13return

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          11
4 5: iload_1
5 6: iconst_2
6 7: if_icmplt    2
7 10return