1. 程式人生 > >Java位元組碼淺析(三)

Java位元組碼淺析(三)

英文原文連結譯文連結,原文作者:James Bloom,譯者:有孚

從Java7開始,switch語句增加了對String型別的支援。不過位元組碼中的switch指令還是隻支援int型別,並沒有增加對其它型別的支援。事實上switch語句對String的支援是分成兩個步驟來完成的。首先,將每個case語句裡的值的hashCode和運算元棧頂的值(譯註:也就是switch裡面的那個值,這個值會先壓入棧頂)進行比較。這個可以通過lookupswitch或者是tableswitch指令來完成。結果會路由到某個分支上,然後呼叫String.equlals來判斷是否確實匹配。最後根據equals返回的結果,再用一個tableswitch指令來路由到具體的case分支上去執行。

public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "a":
            return 0;
        case "b":
            return 2;
        case "c":
            return 3;
        default:
            return 4;
    }
}

這個字串的switch語句會生成下面的位元組碼:

 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

這段位元組碼所在的class檔案裡面,會包含如下的一個常量池。關於常量池可以看下JVM內部細節中的_執行時常量池_一節。

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

注意,在執行這個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,這會兒就應該繼續進行下一個字串的比較)

public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "FB":
            return 0;
        case "Ea":
            return 2;
        default:
            return 4;
    }
}

這段程式碼會生成下面的位元組碼:

 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

###迴圈語句

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指令,它會跳轉到迴圈開始的地方繼續執行,直到分支跳轉的條件滿足才終止。

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指令會判斷區域性變數區中的1號位的變數(也就是i,譯註:區域性變數區從0開始計數,第0位是this)是否大於等於2,如果不是繼續執行,如果是的話跳轉到13行處,結束迴圈。goto指令使得迴圈可以繼續執行,直到條件判斷為真,這個時候會跳轉到緊挨著迴圈體後邊的return指令處。iinc是少數的幾條能直接更新區域性變數區裡的變數的指令之一,它不用把值壓到運算元棧裡面就能直接進行操作。這裡iinc指令把第1個區域性變數(譯註:第0個是this)自增1。

for迴圈和while迴圈在位元組碼裡的格式是一樣的。這並不奇怪,因為每個while迴圈都可以很容易改寫成一個for迴圈。比如上面的while迴圈就可以改寫成下面的for迴圈,當然了它們輸出的位元組碼也是一樣的:

public void forLoop() {
    for(int i = 0; i < 2; i++) {

    }
}

####do-while迴圈

do-while迴圈和for迴圈,while迴圈非常類似,除了一點,它是不需要goto指令的,因為條件跳轉指令在迴圈體的末尾,可以用它來跳轉回迴圈體的起始處。

public void doWhileLoop() {
    int i = 0;
    do {
        i++;
    } while (i < 2);
}

這會生成如下的位元組碼:

0: iconst_0
1: istore_1
2: iinc          1, 1
5: iload_1
6: iconst_2
7: if_icmplt    2
10: return