關於 finally 區塊的執行時機、與 return 的關係、異常處理流程,網路上流傳著許多版本的說法。本文將從 JVM 位元組碼層面,揭示 finally 真實的執行機制,打破常見迷思。
| 常見說法 | 事實 |
|---|---|
| finally 在 return 之後、方法返回前執行 | 編譯器將 finally 程式碼複製到每個 return 之前 |
| return 值暫存區域,finally 執行完再返回 | 返回值在 finally 前已壓入操作數棧/區域變數表 |
| finally 中的 return 會覆蓋 try/catch 的 return | 確實會覆蓋,但這是編譯器生成的程式碼順序導致 |
| JVM 執行到 return 時會檢查是否有 finally | JVM 不「檢查」,完全由編譯器在編譯期安排程式碼順序 |
Java 編譯器(javac)在編譯 try-catch-finally 時,採用程式碼複製策略:將 finally 區塊的程式碼分別插入到:
try 區塊正常結束前(return 或區塊結束)catch 區塊正常結束前public class FinallyBytecodeDemo {
public static int testFinally() {
try {
System.out.println("try block");
return 1;
} catch (Exception e) {
System.out.println("catch block");
return 2;
} finally {
System.out.println("finally block");
}
}
}
反編譯位元組碼(javap -c)關鍵片段:
public static int testFinally();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String try block
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: iconst_1
9: istore_1 // 將返回值 1 存入區域變數表 index 1
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #5 // String finally block
15: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: iload_1 // 從區域變數表載入返回值
19: ireturn
20: astore_2 // catch 區塊開始:異常存入 index 2
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: ldc #6 // String catch block
26: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: iconst_2
30: istore_1 // 返回值 2 存入 index 1
31: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #5 // String finally block
36: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: iload_1 // 載入返回值
40: ireturn
41: astore_3 // 任意異常處理(finally 區塊)
42: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
45: ldc #5 // String finally block
47: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: aload_3
51: athrow // 重新拋出異常
Exception table:
from to target type
0 8 20 Class java/lang/Exception
0 8 41 any
20 31 41 any
istore_1 將返回值存入區域變數表,而非操作數棧any 類型確保所有異常(含 Error)都進入 finallyjsr/ret 子程式呼叫,改用程式碼複製public int finallyReturn() {
try {
return 1;
} finally {
return 2; // 覆蓋 try 的返回值
}
}
// 位元組碼關鍵部分:
// try: iconst_1 → istore_1 → finally 程式碼 → iload_1 → ireturn
// finally: iconst_2 → istore_1 → ireturn (直接返回 2,try 的 iload_1/ireturn 被跳過)
結論:finally 中的 return 會直接返回,導致 try/catch 的返回邏輯被跳過。這是編譯器生成的程式碼順序決定的,非 JVM 執行期決策。
| 場景 | 說明 |
|---|---|
| System.exit(0) | JVM 直接終止,不執行 finally |
| Runtime.getRuntime().halt() | 強制終止,不執行 shutdown hook、finally |
| 死循環/死鎖 | try 區塊無法結束,finally 無機會執行 |
| 執行緒被 kill (SIGKILL) | 作業系統強制終止進程 |
| JVM Crash | Native 層崩潰 |
| 電源斷電 | 硬體層面終止 |
// 這段程式碼:
try (InputStream in = new FileInputStream("a.txt")) {
in.read();
} catch (IOException e) {
e.printStackTrace();
}
// 等價於(簡化):
InputStream in = null;
try {
in = new FileInputStream("a.txt");
in.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) {}
}
}
// 關鍵差異:try-with-resources 的隱含 finally 會抑制異常
// 若 try 和 close() 都拋出異常,try 的異常為主,close() 的為 suppressed
public String trickyFinally() {
StringBuilder sb = new StringBuilder("try");
try {
return sb.toString(); // 返回 "try"
} finally {
sb.append("-finally"); // 修改物件內容,但不影響已返回的字串
}
}
// 實際返回:"try"(字串不可變,toString() 已產生新物件)
public List<String> trickyFinally2() {
List<String> list = new ArrayList<>();
list.add("try");
try {
return list; // 返回列表引用
} finally {
list.add("finally"); // 修改列表內容,呼叫端可見!
}
}
// 實際返回:["try", "finally"](同一個列表物件)
finally 的執行機制本質上是編譯期程式碼複製,而非執行期 JVM 的特殊邏輯。理解位元組碼層面的實作,有助於避免常見陷阱,編寫更健壯的異常處理程式碼。記住核心原則:finally 專注資源清理,不要改變控制流向。