finally 看似簡單,實則暗藏 JVM 層面的精妙設計:子程式模型、返回值暫存、異常表處理、控制流覆蓋規則。多數開發者只知「一定執行」,卻不知「何時執行、怎麼執行、為何有時不執行」。本文基於《JVM 規格》第 7.13 節 Compiling finally,結合字節碼實測,徹底講透 finally 的生死邊界。
| 場景 | 代碼示例 | 原因 |
|---|---|---|
| try 前 return/拋異常 | int m() { return 1; try { ... } finally { ... } } | try 區塊未進入,對應 finally 無綁定執行點 |
System.exit(0) | try { System.exit(0); } finally { ... } | JVM 進程直接終止,無機會執行任何 Java 代碼 |
| 執行緒被 interrupt/kill | 線程在 try/catch 中被強制中止 | OS 級終止,finally 無機會跑 |
| 硬體故障/斷電 | — | 物理層面不可抗力 |
官方文檔原文(The Java Tutorials):
「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.」
JVM 將 finally 區塊編譯為子程式,在每個控制流離開點(正常返回、異常拋出、return/throw/break/continue)處內聯插入子程式調用(jsr/ret 指令,Java 6+ 改用複製展開)。
以 try { return i; } finally { i++; } 為例,字節碼邏輯:
// 簡化字節碼流程
iconst_1 // i = 1
istore_0 // 區域變數 0 存 i
iload_0 // 載入 i 準備返回
istore_1 // 【關鍵】暫存返回值到區域變數 1
iinc 0, 1 // 執行 finally: i++
iload_1 // 恢復暫存的返回值 (仍為 1)
ireturn // 返回 1
結論:finally 中對變數的修改不影響已暫存的返回值。這就是 return i; finally { i++; } 仍返回 1 的根因。
Exception table:
from to target type
2 4 9 any // try 區塊異常 → 目標 9
// 目標 9 邏輯:
astore_2 // 存異常引用到區域變數 2
iinc 0, 1 // 執行 finally
aload_2 // 恢復異常引用
athrow // 重新拋出
異常物件在 finally 執行前暫存,執行後恢復拋出,保證異常傳播不中斷。
int m() {
try { return 1; }
finally { return 2; } // 實際返回 2
}
JLS 規則:finally 完成時若因 return/throw 離開,則新理由覆蓋舊理由,舊返回值/異常被丟棄。
int m() {
try { return 1; }
finally { throw new RuntimeException(); } // 拋出異常,返回值丟失
}
void m() {
for (int i=0; i<10; i++) {
try { if (i==5) break; }
finally { System.out.println("finally"); } // break 前執行
}
}
break/continue 無返回值,故無暫存機制,finally 執行後直接轉移控制流。
List getList() {
List list = new ArrayList<>();
try { return list; }
finally { list.add("polluted"); } // 呼叫端拿到的 list 已被污染!
}
對策:返回前防禦性複製,或用不可變集合 Collections.unmodifiableList。
try (AutoCloseable r1 = () -> { throw new Exception("close1"); };
AutoCloseable r2 = () -> { throw new Exception("close2"); }) {
throw new Exception("try");
}
// 實際拋出:try 異常,close2 異常被抑制(addSuppressed),close1 異常再被抑制
多資源關閉異常以最後關閉者優先抑制前者,try 區塊異常為主異常。
close()、unlock()、release(),禁用 return/throw/break/continue。catch 處理業務異常,finally 只管資源釋放,職責單一。
控制流進入 try
│
├─ 正常執行完 → 暫存返回值 → 執行 finally → 恢復返回值 → 返回
│
├─ 遇到 return/throw → 暫存返回值/異常 → 執行 finally
│ │
│ ├─ finally 正常結束 → 恢復 → 返回/拋出
│ │
│ └─ finally 再次 return/throw → 【覆蓋】以新值為準
│
└─ 拋出異常 → 異常表匹配 catch
│
├─ catch 處理完 → 暫存返回值 → 執行 finally → 恢復 → 返回
│
├─ catch 再拋異常 → 暫存異常 → 執行 finally → 恢復 → 拋出
│
└─ catch return → 暫存返回值 → 執行 finally → (同 return 分支)
徹底理解這張圖,你就擁有了「finally 透視眼」——不再被面試官、Code Review、線上 Bug 繞進去。