⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

面试考察点

这道题是个”送分题陷阱”——回答”一定执行”直接丢分,面试官考的就是你知不知道它会”失约”:

  1. 异常处理基础是否扎实 – finally 的语义你真理解了,还是只背了个结论?
  2. JVM 底层执行流程 – finally 在字节码层面是怎么保证的?什么情况下这个保证会失效?
  3. 系统级认知 – System.exit()、守护线程、kill -9,这些场景能不能信手拈来。
  4. 实战踩坑经验 – finally 里写 return 的坑踩过没?资源清理的正确姿势知道吗?

核心答案

不一定。 绝大多数情况下会执行,但以下场景 finally 直接”罢工”:

  1. System.exit() 终止 JVM – 最常见的”失约”场景,JVM 都没了,还执行个啥。
  2. 线程被强行终止 – 比如调了已废弃的 Thread.stop(),线程直接死亡,finally 来不及跑。
  3. 守护线程中 – 所有非守护线程结束时 JVM 直接退出,守护线程的 finally 可能执行到一半就被掐断。
  4. try 块里死循环或死锁 – 永远出不了 try,自然到不了 finally。
  5. 操作系统层面 kill -9 – 进程都被干掉了,finally 只能默默躺平。

深度解析

原理/机制

finally 的”一定执行”靠的是 编译器在字节码层面的保证:编译器会把 finally 块的代码复制到 try 和每个 catch 块的正常出口、异常出口之后。相当于 JVM 在离开 try-catch 前,必须”绕路”执行一遍 finally。

但这个保证有前提:JVM 还活着,且执行流还在走。 上面提到的所有例外,本质上都是”JVM 被干掉”或”执行流被卡死”,字节码都跑不了了,finally 自然也没戏。

代码示例

public class FinallyNotExecuteDemo {
public static void main(String[] args) throws Exception {
// 逐个取消注释运行,观察 finally 是否执行
case1_SystemExit();
// case2_InfiniteLoop();
// case3_DaemonThread();
}

// ===== 情况 1:System.exit() —— 最常见的"失约" =====
static void case1_SystemExit() {
try {
System.out.println("[try] 正常执行");
System.exit(0); // JVM 直接关闭,后面的代码全废
} finally {
System.out.println("[finally] 你永远看不到我"); // 不会执行
}
}

// ===== 情况 2:死循环 —— 永远出不了 try =====
static void case2_InfiniteLoop() {
try {
System.out.println("[try] 进入死循环...");
while (true) {
// CPU 在这里转圈,finally 永远等不到
}
} finally {
System.out.println("[finally] 你永远看不到我"); // 不会执行
}
}

// ===== 情况 3:守护线程 —— JVM 不等你跑完 =====
static void case3_DaemonThread() throws Exception {
Thread daemon = new Thread(() -> {
try {
System.out.println("[daemon-try] 守护线程启动");
Thread.sleep(3000); // 模拟耗时操作
} catch (InterruptedException e) {
// 被中断
} finally {
// 主线程结束后 JVM 退出,这行可能执行不到
System.out.println("[daemon-finally] 守护线程的善后工作");
}
});
daemon.setDaemon(true); // 关键:设为守护线程
daemon.start();
Thread.sleep(100); // 主线程稍等一下就结束
System.out.println("[main] 主线程结束,JVM 即将退出");
// 主线程结束 → 没有非守护线程了 → JVM 退出 → daemon 的 finally 被掐断
}
}

最佳实践

  1. 资源清理首选 try-with-resources:JDK 7+ 的标准姿势。比 try-finally 更安全,异常信息也更完整。

    // ❌ 老写法:finally 里 close 还可能抛异常,异常套异常
    FileInputStream fis = null;
    try {
    fis = new FileInputStream("file.txt");
    // 使用资源
    } finally {
    if (fis != null) fis.close(); // 这里抛异常会吞掉 try 中的原始异常!
    }

    // ✅ 正确姿势:try-with-resources,异常信息完整保留
    try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用资源,退出时自动 close
    } catch (IOException e) {
    // close 抛的异常会作为 suppressed exception 附加,不会丢失
    e.getSuppressed(); // 可以拿到 close 时的异常
    }
  2. finally 里别搞复杂逻辑:如果 finally 抛了新异常,会吞掉 try/catch 中的原始异常。排查 bug 时你会想砸键盘。

  3. 守护线程里别做关键清理:JVM 退出时不会等守护线程跑完 finally,重要的持久化操作放在非守护线程里。

常见误区

  • 误区一:”finally 在 return 之后执行” – 错。try 中碰到 return 时,JVM 会先把返回值存到临时变量,然后执行 finally,最后再返回临时变量的值。所以 finally 里改基本类型变量不影响返回值,但修改引用对象的内部状态是生效的。

    // 基本类型:finally 改不了返回值
    static int testPrimitive() {
    int i = 0;
    try {
    return i; // 1. 把 i 的值(0)复制到临时槽
    } finally {
    i = 10; // 2. 改了 i,但临时槽里还是 0
    }
    // 返回 0,不是 10!
    }

    // 引用类型:finally 能改对象内部状态!
    static List<String> testReference() {
    List<String> list = new ArrayList<>();
    try {
    list.add("try");
    return list; // 1. 把 list 引用复制到临时槽(指向同一个对象)
    } finally {
    list.add("finally"); // 2. 通过引用修改了对象内容,临时槽里的引用指向的还是这个对象
    }
    // 返回 ["try", "finally"]!因为引用类型传的是地址,finally 操作的是同一个对象
    }
  • 误区二:”finally 总能进行资源回收” – System.exit() 或 JVM 崩溃时,finally 里的 close() 也是白搭。关键资源的回收不能只靠 finally,该用 try-with-resources 就用,该加 Shutdown Hook 就加。

文章目录
  1. 1. 面试考察点
  2. 2. 核心答案
  3. 3. 深度解析
    1. 3.1. 原理/机制
    2. 3.2. 代码示例
    3. 3.3. 最佳实践
    4. 3.4. 常见误区