從字節(jié)碼看try catch finally的return如何執(zhí)行

測試代碼很簡單,如下:
Test.java

public class Test {
    public int get() {
        try{
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            return 1;
        } finally {
            return 2;
        }
    }
}

盡量簡單的代碼,用以說明問題。

javac Test.java

編譯后產(chǎn)生Test.class,打開

cafe babe 0000 0034 0018 0a00 0500 1107
0012 0a00 0200 1307 0014 0700 1501 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0003 6765 7401 0003 2829
4901 000d 5374 6163 6b4d 6170 5461 626c
6507 0012 0700 1601 000a 536f 7572 6365
4669 6c65 0100 0954 6573 742e 6a61 7661
0c00 0600 0701 0013 6a61 7661 2f6c 616e
672f 4578 6365 7074 696f 6e0c 0017 0007
0100 0454 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 136a 6176
612f 6c61 6e67 2f54 6872 6f77 6162 6c65
0100 0f70 7269 6e74 5374 6163 6b54 7261
6365 0021 0004 0005 0000 0000 0002 0001
0006 0007 0001 0008 0000 001d 0001 0001
0000 0005 2ab7 0001 b100 0000 0100 0900
0000 0600 0100 0000 0100 0100 0a00 0b00
0100 0800 0000 6400 0100 0400 0000 1003
3c05 ac4c 2bb6 0003 043d 05ac 4e05 ac00
0300 0000 0200 0400 0200 0000 0200 0d00
0000 0400 0b00 0d00 0000 0200 0900 0000
1a00 0600 0000 0400 0200 0900 0400 0500
0500 0600 0900 0700 0b00 0900 0c00 0000
0a00 0244 0700 0d48 0700 0e00 0100 0f00
0000 0200 10

cafe babe這樣還是比較難懂的,我們當(dāng)然也可以強行自己去解釋過來。但是有更方便的方法:

javap -verbose Test.class

利用javap工具幫助解釋一下字節(jié)碼,顯示如下:

$ javap -verbose Test.class
Classfile /E:/workspace/java/Test.class
  Last modified 2018-1-29; size 405 bytes
  MD5 checksum f8f6002de3931b2e95125679f2ce1f6c
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // java/lang/Exception
   #3 = Methodref          #2.#19         // java/lang/Exception.printStackTrace                          :()V
   #4 = Class              #20            // Test
   #5 = Class              #21            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               get
  #11 = Utf8               ()I
  #12 = Utf8               StackMapTable
  #13 = Class              #18            // java/lang/Exception
  #14 = Class              #22            // java/lang/Throwable
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #6:#7          // "<init>":()V
  #18 = Utf8               java/lang/Exception
  #19 = NameAndType        #23:#7         // printStackTrace:()V
  #20 = Utf8               Test
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Throwable
  #23 = Utf8               printStackTrace
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>                          ":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int get();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.prin                          tStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any
      LineNumberTable:
        line 4: 0
        line 9: 2
        line 5: 4
        line 6: 5
        line 7: 9
        line 9: 11
      StackMapTable: number_of_entries = 2
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
}
SourceFile: "Test.java"

這樣看就清晰很多,minor version 0、major version 52是支持的JDK版本,也就是JDK1.8,Constant pool是著名的常量池,還有訪問權(quán)限public = ACC_PUBLIC。
本文重點不是這些,重點看get()方法的字節(jié)碼。

descriptor: ()I

表示沒有參數(shù),返回值是int型。

flags: ACC_PUBLIC

表示public方法。重點看Code的部分:

    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.prin                          tStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any
      LineNumberTable:
        line 4: 0
        line 9: 2
        line 5: 4
        line 6: 5
        line 7: 9
        line 9: 11
      StackMapTable: number_of_entries = 2
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]

分析一下其中的附加屬性:

stack=1, locals=4, args_size=1

我們知道一個方法在虛擬機中對應(yīng)一個棧幀,一個棧幀內(nèi)包含了操作數(shù)棧、局部變量表等。

  1. stack=1就表示著操作數(shù)棧的最大深度是1。

  2. locals表示局部變量表需要的存儲空間,單位是Slot,Slot是虛擬機為局部變量表分配內(nèi)存使用的最小單位。對于byte、char、float、int、short、boolean和returnAddress等長度不超過32位的數(shù)據(jù)類型,每個局部變量占用1個Slot,而double和long這兩種64位的數(shù)據(jù)類型則需要兩個Slot來存放。方法參數(shù)、顯示異常處理器的參數(shù)(包括實例方法中的隱藏參數(shù)“this”)、方法體中定義的局部變量都需要使用局部變量表來存放。

依以上觀點,this算一個,i算一個,Exception e算一個,但locals=4?
稍后我們一起看字節(jié)碼指令再來揭曉。

  1. args_size=1,表示方法參數(shù)數(shù)量是1。get()方法明明沒有參數(shù),這1,就是前文提到的“實例方法中隱藏參數(shù)this”。在任何實例方法里面,都可以通過this關(guān)鍵字訪問到此方法所屬對象。實現(xiàn)非常簡單:僅僅是通過Javac編譯器編譯的時候把對this關(guān)鍵字的訪問轉(zhuǎn)變?yōu)閷σ粋€普通參數(shù)的訪問,任何在虛擬機調(diào)用此方法時自動傳入此參數(shù)而已。

static修飾的靜態(tài)方法屬于類方法,就不存在此參數(shù)了,靜態(tài)方法的args_size和locals就會從0開始計數(shù)。

Code的尾部跟著3個標(biāo)簽:Exception table、LineNumberTable、StackMapTable。接下來理解一下:

  1. Exception table
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any

JVM8虛擬機規(guī)范【https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3】中的Code屬性的標(biāo)準(zhǔn)結(jié)構(gòu)如下:

    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];

看代碼就已經(jīng)比較好理解了:從start_pc(開始的pc指針)執(zhí)行到end_pc(結(jié)束的pc指針),假如發(fā)生了catch_type類型的異常,就跳轉(zhuǎn)到異常處理的pc指針處執(zhí)行(handler_pc)
從0到2執(zhí)行,要是有java/lang/Exception異常,就跳轉(zhuǎn)到target(目標(biāo))4行執(zhí)行;假如是其他的異常(any),就跳到13行執(zhí)行;同時發(fā)生java/lang/Exception異常后,執(zhí)行4-11行時假如又發(fā)生任意異常(any),就跳到13行執(zhí)行。
jvm虛擬機就是這樣通過異常表來執(zhí)行的。具體我們待會再進一步到Code中看怎么執(zhí)行。

  1. LineNumberTable

LineNumberTable屬性用于描述Java源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系。它并不是 運行時必須的屬性,但默認(rèn)會生成到Class文件之中,可以在javac中分別使用-g:none或-g:lines選項來取消或要求生成這項信息。如果選擇不生成LineNumberTable屬性,對程序運行產(chǎn)生最主要的影響就是當(dāng)拋出異常時,堆棧中將不會顯示出錯的行號,并且在調(diào)試程序的時候,也無法按照源碼行來設(shè)置斷點。

  1. StackMapTable
    StackMapTable和Class文件的字節(jié)碼合法性驗證相關(guān),是JDK1.7之后不可或缺的一個屬性?!禞ava虛擬機規(guī)范(Java SE 7版)》花費了整整120頁來講解描述,但與本文內(nèi)容無關(guān),略去不講。

下面一起來看重點部分:代碼執(zhí)行

         0: iconst_0
         1: istore_1
         2: iconst_2
         3: ireturn
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Exception.printStackTrace:()V
         9: iconst_1
        10: istore_2
        11: iconst_2
        12: ireturn
        13: astore_3
        14: iconst_2
        15: ireturn
      Exception table:
         from    to  target type
             0     2     4   Class java/lang/Exception
             0     2    13   any
             4    11    13   any

最好是結(jié)合原本代碼一起看

public class Test {
    public int get() {
        try{
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            return 1;
        } finally {
            return 2;
        }
    }
}

0: iconst_0
把第0個int型也就是0推送到操作數(shù)棧棧頂。


1: istore_1
那意思就是把棧頂?shù)?存入局部變量表的位置1,為什么不是從0開始?(提示“this”)

2: iconst_2
按理說try已經(jīng)走完了,就一個return 0,應(yīng)該return才對。但是沒有,第二個int型數(shù)據(jù),不就是finally里面的2,把finally里面的2推到了棧頂。所以知道為什么把0存入局部變量表了,因為虛擬機分配的操作數(shù)棧深度是1!

3: ireturn
返回int型,操作數(shù)棧棧頂?shù)臄?shù)據(jù),也就是2。所以說明try里、finally都有return時,執(zhí)行finally里的return。


正常執(zhí)行的部分已經(jīng)看完了。異常的部分,使用異常表進行處理,具體執(zhí)行邏輯前面已經(jīng)討論過,這里我們再來順著邏輯梳理一遍。
從異常表我們知道,0-2是正常執(zhí)行(3就return了,所以不算在里面),假如有Exception類型的異常,就跳轉(zhuǎn)到4執(zhí)行。接下來從4行開始看。
4: astore_1
上面我們知道,曾經(jīng)把0存入局部變量表的位置1。這里是因為0的作用域是try對應(yīng)的大括號,catch里面0已經(jīng)無效,所以局部變量表也就不再為它保持內(nèi)存。這個棧頂引用類型也就是拋出來的異常Exception e了。

5: aload_1
把剛剛存入局部變量表的引用類型e又加載到操作數(shù)棧棧頂,因為有一行:e.printStackTrace();需要用到。

6: invokevirtual #3 // Method java/lang/Exception.printStackTrace:()V
調(diào)用實例方法,占據(jù)了三行。

9: iconst_1
直接9行,以第0行類推,就是把代碼中第1個int型也就是catch的1推到操作數(shù)棧棧頂,也就是1。
10: istore_2
以1行類推,也就是把棧頂?shù)?存到局部變量表位置2(位置0是this,位置1是對象e)
11: iconst_2
把第二個int型finally的2推送到棧頂
12: ireturn
return掉棧頂?shù)臄?shù)據(jù)(也就是finally的2)


由此可見,即使catch中有return,執(zhí)行的也是finally中的return。

由異常表中,我們知道,編譯器編譯過后,又考慮了拋出的異常不是Exception類型的情況,無論try或catch出異常時,都去執(zhí)行13行的代碼。
13: astore_3
類比5行,這里是把不明類型的異常存入局部變量表第3個位置。這個異常時可能在catch中出現(xiàn)的,catch中我們占用了局部變量表位置1和2,所以用3是最保險的。
14: iconst_2
依舊是把finally的2推到棧頂
15: ireturn
把棧頂?shù)?return出去

至此,我們就弄明白了try catch finally中都有return的情況下,虛擬機會如何去執(zhí)行return代碼。那就是最終只走finally的return。






最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容