【java_基礎深入】從語言規範、字節碼指令、jvm編譯 理解finally語句塊細節

一、finally語句塊執行的外在表現

1.1. Gosling 親自對 finally 的描述

a finally clause is always entered with a reason. That reason may be that the try code finished normally, that it executed a control flow statement such as return, or that an exception was thrown in code executed in the Try block. The reason is remembered when the finally clause exits by falling out the bottom. However, if the finally block creates its own reason to leave by executing a control flow statement (such as break or return) or by throwing an exception, that reason supersedes the original one, and the original reason is forgotten. For example, consider the following code:

try {
	// … do something …
	return 1;
}  finally {
	return 2;
}

When the Try block executes its return, the finally block is entered with the “reason” of returning the value 1. However, inside the finally block the value 2 is returned, so the initial intention is forgotten. In fact, if any of the other code in the try block had thrown an exception, the result would still be to return 2. If the finally block did not return a value but simply fell out the bottom, the “return the value 1 ″ reason would be remembered and carried out.

1. 2. 根據對Java的瞭解進行的翻譯

進入finally子句的原因如下:

  1. 語句正常執行
  2. 被執行在流程控制語句中
  3. 執行語句拋出異常 產生的原因會被記錄到finally語句執行時。

進一步,finally子句因某種情況跳出了當前執行的流程控制語句的情況有:

  1. break
  2. return
  3. 拋出異常

以上三種情況會取代原有的執行語句(try中的return, break, 拋出異常) , 第三部分將完成這個實驗

以下程序爲例 :

try {
	// … do something …
	return 1;
} finally {
	return 2;
}

當 try 語句塊中執行return語句, finally語句會拿到 1 這個值, 然而,finally內的語句 return 2 會在
return 1前執行。最初的return 1 語句會被覆蓋。 值得注意的是, 上面的例子程序中:

  1. 在 try 語句中發生任意異常,finally內的語句 return 2始終會執行。
  2. 如果 finally 語句塊中沒有 返回值一個值, 那麼return 1;將正常執行
    返回一個值,是一個籠統的說法,第二部分將說明】

1.3. 例子程序實驗

1.3.1 try中無異常, try 和 finally 同時持有return語句

    public static int testFinallyWithReturn() {
        try {
            return  10;
        } finally {
            return  1000;
        }
    }

返回1000

1.3.2 try中有異常, try 和 finally 同時持有return語句

    public static int testFinallyWithExceptionButReturn(int a) {
        try {
       		// ArithmeticException 會被finally的return語句吃掉
            a = a / 0;  
            return  10;
        } finally {
            // 1. return 可以覆蓋拋異常
            // 2. throw 語句和return 語句不可同級並列
            return  1000;
        }
    }

不拋出異常,同時返回1000

1.3.3 try中有異常, try 和 finally 同時拋出異常

    public static int testFinallyWithNotReturn(int a) {
        try {
            a = a / 0;  // ArithmeticException 會被finally的return語句吃掉
            return a + 10;
        } finally {
            // 1. return 可以覆蓋拋異常
            // 2. throw 語句和return 語句不可同級並列
            throw new RuntimeException("finally語句塊中的異常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally語句塊中的異常

  • 使用更直觀的測試用例測試
    public static int testCatchFinallyWithException(int a) {
        try {
            throw new RuntimeException("try語句塊中的異常");
            //a = a / 0;
        } finally {
            throw new RuntimeException("finally語句塊中的異常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally語句塊中的異常

  • 再演進,跟預期一樣
    public static int testCatchFinallyWithException(int a){
        try {
            throw new RuntimeException("try語句塊中的異常");
        } catch (Exception e) {
            // e.printStackTrace();
            throw new RuntimeException("catch語句中的異常");
        } finally {
            throw new RuntimeException("finally語句塊中的異常");
        }
    }

Exception in thread “main” java.lang.RuntimeException: finally語句塊中的異常

  • 待填的坑:雖然跟預期一樣,但是輸出堆棧信息,try 和 catch 確實是拋出了異常。
  • 不打印堆棧,就只有finally的異常
    public static int testCatchFinallyWithException(int a){
        try {
            throw new RuntimeException("try語句塊中的異常");
        } catch (Exception e) {
            e.printStackTrace();
            try {
                throw new RuntimeException("catch語句中的異常");
            } catch (Exception e2){
                e2.printStackTrace();
            }
        } finally {
            throw new RuntimeException("finally語句塊中的異常");
        }
    }

[打印堆棧信息才能看到前兩句]
java.lang.RuntimeException: try語句塊中的異常
java.lang.RuntimeException: catch語句中的異常
Exception in thread “main” java.lang.RuntimeException: finally語句塊中的異常

1.3.4 finally 中執行 continue 和 break

    public static int testFinallyWithContinue(int a) {
        int i = 0;
        while (i < 2) {
            try {
           		// ArithmeticException 會被finally的continue語句吃掉
                a = a / 0;  
            } finally {
                i++;
                continue;
            }
        }
        return i;
    }
    public static int testFinallyWithBreak(int a) {
        int i = 0;
        while (i < 2) {
            try {
                // ArithmeticException 會被finally的break語句吃掉
                a = a / 0;
            } finally {
                break;
            }

        }
        return i;
    }

都不拋異常


二、finally 外在表現的引申

2.1 對 Gasling 描述的補充

經試驗,結論與預期相符。其中Gasling的一個說法較爲籠統(或是翻譯不到位) :

" finally 語句塊中 ***返回值一個值***可以覆蓋原有的值"

其中 break 和 continue 並沒有返回一個值,但是也成功把try語句中的異常給吃掉了
另外一種更好理解的解釋是(轉載)

“ 其中 return 和 throw 把程序控制權轉交給它們的調用者(invoker),而 break 和 continue 的控制權是在當前方法內轉移”

2.2 官網《 The Java Tutorials 》印證了這種說法

The finally Block
The finally block always executes when the try block exits. This ensures that the finally block is executed even if an
unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to
avoid having cleanup code accidentally bypassed by a return,continue, or break. Putting cleanup code in a finally block is
always a good practice, even when no exceptions are anticipated.
Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if
the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the
application as a whole continues.

三、究其原因

3.1 編譯爲字節碼指令

public static int getValue() { 
       int i = 1; 
       try { 
            return i; 
       } finally { 
            i++; 
       } 
    } 
}
public static int getValue(); 
 Code: 
  0:    iconst_1 
  1:    istore_0  
  2:    iload_0   
  3:    istore_1  		 // finally相關的指令
  4:    iinc    0, 1    // 詭異的兩段相同指令 -> 下面將圖解這個問題
  7:    iload_1 
  8:    ireturn 
  9:    astore_2 
  10:   iinc    0, 1   // 詭異的兩段相同指令
  13:   aload_2 
  14:   athrow 
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

getValue() : 1

3.2 圖解字節碼指令過程

3.2.1 getValue()正常執行過程

在這裏插入圖片描述

public static int getValue(); 
 Code: 
  0:    iconst_1  // i (操作數)入棧
  1:    istore_0  // index = 0 的 i store 進本地變量表
  2:    iload_0   // index = 0 的 i load 進操作數棧  【此時操作棧爲空】
  3:    istore_1  		// index = 1 的 i store 進本地變量表 (拷貝index = 0 的 i)/* finally 相關的字節碼指令 */
  4:    iinc    0, 1    // index = 0 的 i 自增1  									/* finally 相關的字節碼指令 */
  7:    iload_1 		// index = 1 的 i load 進操作數棧 【此時操作棧元素爲1個】
  8:    ireturn 		// index = 1 的 i 從操作數返回給調用者
  9:    astore_2 		// 異常處理相關
  10:   iinc    0, 1  											 					/* finally 相關的字節碼指令 */
  13:   aload_2 		// 異常處理相關
  14:   athrow 			// 異常處理相關
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

能夠清晰得看出: finally 語句塊相關的字節碼指令被插入到了return 語句之前
finally 語句塊中操作的 i 是 拷貝了 index = 0 的 i 生成 index = 1 的 i 到本地變量表
linc 0,1 說明 自增操作在 index = 0 的 i上 也就是 try 語句中的 i, iload_1 把 index = 0 的 i 壓入操作數棧中
ireturn 中 ***return 了 i操作數棧的棧頂元素 index = 1 的 i *** , 值自然就是1了

總得來說: finally 拷貝了一份X(i) 變量 生成爲Y(i), 對X(i++),返回的卻是Y(i)

3.2.2 getValue()異常執行過程
 Exception table: 
  from   to  target type 
    2     4     9   any  // 如果從 2 到 4 這段指令出現異常,則由從 9 開始的指令來處理。
    9    10     9   any 

在這裏插入圖片描述

3.2.3 getValue() 的 finally 中加入return
  public static int getValue() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
            return i;
        }
    }

jdk1.8編譯後的兩點變化
左:加了return | 右 :未加return
在這裏插入圖片描述

加了return 後 小結
  1. 加了return iload的字節碼指令改變,也就改變了最終返回的不是副本
  2. 異常處理中的athrow變成了ireturn,另一方面解釋了在try語句塊中的異常會給finally中的return吃掉
    所以, 以上代碼返回 2

3.2.3 getValue() 的 finally 的 return 前再加入其他代碼
  • 只加在finally
    和預期相同,try語句塊中同步了finally內新增的代碼。
    在這裏插入圖片描述
  • 加了同名的變量聲明
    • 代碼對應字節碼
      在這裏插入圖片描述
  • 【重點一】關注第8 9 10 12行
 8 iload_1
 9 istore_3
 10 bipush	34 // push一個常數34到操作數棧中
 12 istore	4  // 把34填到slot = 4 的本地變量表,顯然是新增了一個變量

在這裏插入圖片描述
【重點二】返回前,load了finally創建的變量,也就是操作副本,返回副本

17 iload	4
19 ireturn 

在這裏插入圖片描述

  • 進一步可以發現,如果finally內創建了try中的同名變量q, 後面代碼操作的也是finally 在本地方法表新建的q變量
  • 進一步證明了同名變量 + return 的情況下 : 操作副本,返回副本;
字節碼指令 總結
  1. finally 內不加return,操作try中的變量
    1.1 拷貝一份finally中的代碼,插入try return之前
    1.2 先拷貝try中的變量X到本地變量表,成爲Y,再對X操作。
    1.3 【關鍵】返回的是Y,也就是操作了X但是不返回,還是返回了操作前的值Y
    總結:finally內不加return,返回的是finally創建的副本,而finally的所有操作都在原來的值上

  1. finally 內return,操作try中的變量
    2.1 拷貝一份finally中的代碼,插入try return之前
    2.2 操作的原理同1.2
    2.3 【關鍵】load了X,再ireturn。也就不是返回finally創建的副本,是實實在在返回了被操作的X

  1. finally 中return,並聲明和try中同名的變量,操作變量
    3.1 【關鍵】load 並store了try中同名的變量到【新的slot】,操作的是新的slot,也就是新的副本
    同樣是做了拷貝,只是使用的字節碼不一樣
    3.2 原來的ireturn是不變
    3.3 把異常處理中的 throw 修改成了 return
    3.4 【關鍵】load了【新的slot】到操作數棧中,也就是新的副本,最後返回副本
    總結:在finally中聲明瞭try中已有的變量,會針對同名變量的值拷貝一個副本,所有的操作都是針對這個副本。
拓展
沒有 catch 語句,哪來的異常處理呢?(轉載)

其實,即使沒有 catch 語句,Java 編譯器編譯出的字節碼中還是有默認的異常處理的,別忘了,除了需要捕獲的異常,還可能有不需捕獲的異常(如:RunTimeException 和 Error)。
當從 2 到 4 這段指令出現異常時,將會產生一個 exception 對象,並且把它壓入當前操作數棧的棧頂。接下來是 astore_2 這條指令,它負責把 exception 對象保存到本地變量表中 2 的位置,然後執行 finally 語句塊,待 finally 語句塊執行完畢後,再由 aload_2 這條指令把預先存儲的 exception 對象恢復到操作數棧中,最後由athrow指令將其返回給該方法的調用者(main)。

  • 分析異常處理部分
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
 Code: 
  0:    iconst_1  
  1:    istore_0  
  2:    iload_0   		// 2, 3 ,4 任意發生異常, iload_1 和 ireturn 都不執行, 
  						// 也就無法返回 index = 0 的 i, 會跳到9開始,最終到14拋出
  3:    istore_1  		/* finally 在try中插入的字節碼指令 */   
  4:    iinc    0, 1    /* finally 在try中插入的字節碼指令 */ 
  7:    iload_1 		
  8:    ireturn 		
  9:    astore_2 		// 異常處理相關
  10:   iinc    0, 1  	/* finally 預期內的字節碼指令 */
  13:   aload_2 		// 異常處理相關
  14:   athrow 			// 異常處理相關

覆盤

也就解釋了爲什麼try 語句中出現的異常 會給 finally 中的 return 、 break 、 continue 吃掉。
也解釋了 finally 最終return 的值會在 try return 之前,也就是覆蓋了 try 中的return
當然,try 中如果是個函數 如 return function(); 相當於是 int temp = function(); return temp;
function() 會執行,但是temp的值無法返回

輔助材料: finally 相關的編譯底層實現

《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 節 Compiling finally中說道(轉載的翻譯)

實際上,Java 虛擬機會把 finally 語句塊作爲 subroutine(對於這個 subroutine 不知該如何翻譯爲好,乾脆就不翻譯了,免得產生歧義和誤解。)直接插入到 try 語句塊或者 catch 語句塊的控制轉移語句之前。但是,還有另外一個不可忽視的因素,那就是在執行 subroutine(也就是 finally 語句塊)之前,try 或者 catch 語句塊會保留其返回值到本地變量表(Local Variable Table)中。待 subroutine 執行完畢之後,再恢復保留的返回值到操作數棧中,然後通過 return 或者 throw 語句將其返回給該方法的調用者(invoker)。請注意,前文中我們曾經提到過 return、throw 和 break、continue 的區別,對於這條規則(保留返回值),只適用於 return 和 throw 語句,不適用於 break 和 continue 語句,因爲它們根本就沒有返回值。

以上就能解釋字節碼指令的 生成邏輯


參考資料:https://www.ibm.com/developerworks/cn/java/j-lo-finally/

注:文中的圖片和部分翻譯均轉載上述連接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章