閱讀853 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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


最後更新:2017-05-23 18:02:44

  上一篇:go  聊聊JVM的年輕代
  下一篇:go  ClassNotFoundException: 真的會使你的JVM慢下來嗎?