资讯动态

【后端开发】finally 真的一定执行吗?从字节码、异常覆盖到线上资源泄漏讲透 Java finally

发布时间:2026/10/26 20:05:36 来源:尧图企业网站定制
文章目录前言一、先把 finally 的执行规则讲准确1.1 finally 修改局部变量不一定影响返回值1.2 返回对象时finally 修改“对象内容”会生效1.3 finally 中重新给引用赋值不会改变返回对象1.4 finally 里写 return会覆盖 try 里的 return1.5 finally 里的 return 可能吞掉异常1.6 finally 里抛异常会覆盖 try 里的异常二、finally 真的一定执行吗先把边界说清楚2.1 正常离开 tryfinally 通常会执行2.2 System.exitJVM 都要退出了finally 没机会继续执行2.3 死循环不是 finally 不执行而是根本没离开 try2.4 进程被强杀、JVM 崩溃、机器断电finally 不是崩溃恢复机制2.5 守护线程里的 finally不一定来得及执行2.6 shutdown hook 和 finally 不是一回事写在文后 个人主页铁皮哥欢迎关注 作者简介28届校招生后端开发/Agent 方向在学 学习内容Java、Python、计算机视觉、大语言模型、Agent开发 专栏内容从零开始的Claude Code零代码生活持续更新中✨不只背八股更想搞懂为什么这样设计前言finally中的代码一定会被执行吗这个问题应该算是 Java 面试里的经典题了。很多人的第一反应是“不一定System.exit()的时候不会执行。”这个答案当然没错但如果只回答到这里其实有点可惜。因为面试官问finally通常不是想听你背几个特殊情况而是想看你对Java 异常处理机制、方法返回过程、资源释放语义到底理解到什么程度。比如下面这几个问题就比“finally会不会执行”更值得深挖try里已经return了为什么finally还能执行finally里修改变量为什么有时候影响返回结果有时候又不影响try和finally同时抛异常最后到底会保留哪个异常为什么很多线上问题不是因为finally没执行而是因为finally执行了反而把真正的异常覆盖掉了我后来写后端代码多了之后才发现finally真正容易出问题的地方往往不是那些极端场景比如 JVM 崩溃、进程被 kill、System.exit()而是一些看起来很正常的业务代码try{doBusiness();}finally{releaseResource();}这段代码看起来很安全对吧但如果doBusiness()抛了异常releaseResource()也抛了异常最后日志里看到的可能不是业务异常而是资源释放失败的异常。也就是说finally确实执行了但它把真正有价值的现场信息覆盖掉了。这类问题在线上排查时非常恶心。你以为问题出在关闭连接、关闭流、释放锁结果真正的根因可能早就被覆盖了。一、先把 finally 的执行规则讲准确看下面这段代码publicstaticinttest(){inti1;try{returni;}finally{i2;}}这段代码的返回值是多少很多人第一眼会觉得是2因为finally会在return之后执行i被改成了2。但实际返回值是1。这就说明一个问题“finally 在 return 之后执行”这个说法并不严谨。更准确的说法应该是try/catch里的return会先计算返回值但方法不会立刻真正返回在真正返回之前会先执行finally。也就是说finally不是在“方法已经返回之后”执行的。方法一旦真的返回后面的代码就不可能再执行了。它真正发生的位置是计算 return 表达式 ↓ 保存返回值 ↓ 执行 finally ↓ 真正返回之前保存的结果所以刚才那个例子里执行过程大概是这样publicstaticinttest(){inti1;try{returni;// 先把 i 当前的值 1 作为返回结果保存起来}finally{i2;// 再执行 finally但这时改的是局部变量 i}}最终返回的还是之前已经保存好的1。1.1 finally 修改局部变量不一定影响返回值我们再看一个更完整的例子。publicclassFinallyDemo{publicstaticvoidmain(String[]args){System.out.println(test());}publicstaticinttest(){inti1;try{returni;}finally{i2;System.out.println(finally 中的 i i);}}}输出结果是finally 中的 i 2 1这段输出其实很关键。它说明两件事第一finally确实执行了。第二finally修改了局部变量i但最终返回值没有被改成2。原因就是前面说的return i执行时返回值已经先被计算并保存起来了。后面finally里再改i改的是局部变量本身不是之前保存好的返回值。1.2 返回对象时finally 修改“对象内容”会生效上面的例子是基本类型那如果返回的是对象呢看下面这段代码importjava.util.ArrayList;importjava.util.List;publicclassFinallyDemo{publicstaticvoidmain(String[]args){System.out.println(test());}publicstaticListStringtest(){ListStringlistnewArrayList();try{list.add(try);returnlist;}finally{list.add(finally);}}}输出结果是[try, finally]这次finally中的修改生效了。是不是和前面的结论矛盾其实不矛盾。关键在于return list保存的是对象引用。这个引用指向的是堆内存中的那个ArrayList对象。执行return list时先把这个引用保存起来。然后进入finally执行list.add(finally);这行代码并没有让list指向一个新对象而是在修改原来那个ArrayList对象的内容。最终方法返回的还是之前保存的那个引用。只不过这个引用指向的对象内容已经被finally改过了。1.3 finally 中重新给引用赋值不会改变返回对象再看一个容易误判的例子importjava.util.ArrayList;importjava.util.List;publicclassFinallyDemo{publicstaticvoidmain(String[]args){System.out.println(test());}publicstaticListStringtest(){ListStringlistnewArrayList();try{list.add(try);returnlist;}finally{listnewArrayList();list.add(finally);}}}你觉得输出是什么答案是[try]不是[finally]也不是[try, finally]。原因也很好理解。在try中执行returnlist;此时返回值已经保存了原来那个ArrayList对象的引用。到了finally里listnewArrayList();list.add(finally);这两行代码只是让局部变量list指向了一个新的ArrayList对象。但是方法真正要返回的引用早就在return list那一步保存好了。你后面让局部变量list指向新对象并不会改变之前保存的返回引用。1.4 finally 里写 return会覆盖 try 里的 return前面几个例子里finally只是修改变量。那如果finally自己也写了return呢publicclassFinallyDemo{publicstaticvoidmain(String[]args){System.out.println(test());}publicstaticinttest(){try{return1;}finally{return2;}}}输出结果是2这次finally真的改变了返回结果。原因也很直接finally自己发起了新的返回动作它会覆盖掉try中原本准备返回的结果。这段代码的执行过程大概是try 中准备返回 1 ↓ 进入 finally ↓ finally 中直接 return 2 ↓ 方法最终返回 2所以这里要非常明确地提醒一句不要在 finally 中写 return。这不是代码风格问题而是很容易制造隐藏 bug。因为读代码的人看到try里有一个return 1第一反应会以为方法返回1。但真正的返回结果却被finally改成了2。更糟糕的是finally里的return不只会覆盖返回值还可能覆盖异常。1.5 finally 里的 return 可能吞掉异常看下面这段代码publicclassFinallyDemo{publicstaticvoidmain(String[]args){test();System.out.println(程序继续执行);}publicstaticvoidtest(){try{thrownewRuntimeException(try 中的异常);}finally{return;}}}输出结果是程序继续执行try里明明抛出了异常thrownewRuntimeException(try 中的异常);但这个异常并没有传出去。原因就是finally中的return;把原本的异常流程覆盖掉了。这就很危险了。因为异常最重要的作用是告诉调用方这里出问题了。而finally中的return会让方法看起来像是“正常结束”导致真正的错误被悄悄吞掉。如果这类代码出现在真实项目里排查起来会非常难受。比如publicvoidhandleOrder(){try{createOrder();deductStock();sendMessage();}finally{return;}}这段代码看起来只是做了一个收尾但实际上只要finally里有return前面任何业务异常都可能被吞掉。订单创建失败了库存扣减失败了消息发送失败了调用方可能完全感知不到。所以在实际开发里finally中应该避免出现任何改变控制流的语句比如return;thrownewRuntimeException();break;continue;尤其是return基本可以认为是finally里的高危写法。1.6 finally 里抛异常会覆盖 try 里的异常除了returnfinally中重新抛异常也很危险。看这个例子publicclassFinallyDemo{publicstaticvoidmain(String[]args){test();}publicstaticvoidtest(){try{thrownewRuntimeException(业务异常);}finally{thrownewRuntimeException(资源释放异常);}}}最终你在控制台看到的异常通常是Exception in thread main java.lang.RuntimeException: 资源释放异常而不是业务异常也就是说try中原本的异常被finally中的新异常覆盖了。在真实业务场景中很多资源释放代码可能长这样Connectionconnectionnull;try{connectiondataSource.getConnection();doBusiness(connection);}finally{connection.close();}如果doBusiness(connection)抛了一个业务异常接着connection.close()又抛了一个关闭异常那么最终暴露出来的可能是关闭异常而不是最开始真正导致业务失败的异常。二、finally 真的一定执行吗先把边界说清楚2.1 正常离开 tryfinally 通常会执行先看最普通的情况publicstaticvoidtest(){try{System.out.println(try);}finally{System.out.println(finally);}}输出结果很好理解try finally这说明try正常执行结束时finally会执行。如果try里抛异常呢publicstaticvoidtest(){try{System.out.println(try);thrownewRuntimeException(业务异常);}finally{System.out.println(finally);}}输出会先看到try finally然后异常继续往外抛。2.2 System.exitJVM 都要退出了finally 没机会继续执行最经典的反例就是System.exit()。publicclassFinallyExitDemo{publicstaticvoidmain(String[]args){try{System.out.println(try);System.exit(0);}finally{System.out.println(finally);}}}通常输出结果只有tryfinally没有打印。原因也不复杂。System.exit(0)不是普通的return它表示请求终止当前 JVM。执行到这里时程序不会再按照普通 Java 控制流继续往下走。2.3 死循环不是 finally 不执行而是根本没离开 try再看一个更容易被误解的例子publicstaticvoidtest(){try{while(true){// 一直循环}}finally{System.out.println(finally);}}这段代码里的finally也不会执行。但它和System.exit()的原因不一样。System.exit()是执行流被 JVM 退出打断了。死循环则是执行流一直卡在try里面根本没有走到离开try的那一步。finally的语义是当控制流准备离开 try 时先执行 finally。但现在的问题是控制流一直没有离开try。2.4 进程被强杀、JVM 崩溃、机器断电finally 不是崩溃恢复机制再往外看一层。如果程序运行过程中进程被直接强杀比如 Linux 下执行kill-9pid或者 JVM 自身崩溃甚至机器直接断电那么finally也不能保证执行。因为这些情况已经不是 Java 代码内部的正常控制流变化了而是运行环境直接中断。finally再特殊它也只是 Java 语言层面的机制。它没有能力在进程已经被操作系统强制杀掉之后继续执行一段 Java 代码。2.5 守护线程里的 finally不一定来得及执行还有一种容易被忽略的情况守护线程。先看代码publicclassDaemonFinallyDemo{publicstaticvoidmain(String[]args){ThreadthreadnewThread(()-{try{System.out.println(daemon start);Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}finally{System.out.println(daemon finally);}});thread.setDaemon(true);thread.start();System.out.println(main end);}}这段代码里子线程被设置成了守护线程thread.setDaemon(true);主线程启动它之后很快打印main end然后主线程结束。如果 JVM 中已经没有其他非守护线程了那么 JVM 可以直接退出。这个时候守护线程可能还在sleep根本没来得及走到finally。所以你可能只看到main end daemon start但不一定能看到daemon finally这不是因为finally在守护线程里语义变了而是因为 JVM 不会为了守护线程继续存活。普通线程还没结束JVM 会等。只剩守护线程时JVM 可以退出。所以守护线程里的finally不适合承担关键资源释放。比如异步日志刷新、临时文件清理、消息补偿、连接关闭如果这些动作真的重要就不应该只依赖守护线程里的finally。更稳妥的做法是设计明确的关闭流程比如应用停止时主动关闭线程池、等待任务完成、刷新缓冲区、释放连接池。finally可以作为线程内部的收尾逻辑但不能保证在 JVM 退出时一定跑完。2.6 shutdown hook 和 finally 不是一回事讲到 JVM 退出很多人会想到shutdown hook。比如publicclassShutdownHookDemo{publicstaticvoidmain(String[]args){Runtime.getRuntime().addShutdownHook(newThread(()-{System.out.println(shutdown hook);}));System.out.println(main end);}}shutdown hook是 JVM 关闭时触发的一类回调。它可以用于做一些应用级别的收尾动作比如关闭线程池、刷新日志、释放连接池、通知注册中心下线。但它和finally不是一回事。finally是当前线程、当前代码块退出前的收尾逻辑。shutdown hook是 JVM 进入关闭流程时执行的回调逻辑。一个是局部代码块级别。一个是 JVM 进程级别。举个例子try{MDC.put(traceId,traceId);doBusiness();}finally{MDC.clear();}这里的finally适合清理当前请求线程里的上下文。它关注的是这一小段业务代码退出时该把线程变量清掉。而shutdown hook更像这样Runtime.getRuntime().addShutdownHook(newThread(()-{threadPool.shutdown();dataSource.close();logSystem.flush();}));它关注的是整个应用进程要结束了能不能做一些统一收尾。写在文后期待您的一键三连如果有什么问题或建议欢迎在评论区交流

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑