警惕!在try块里关闭流,是典型的资源泄露“元凶”

张开发
2026/4/14 21:22:07 15 分钟阅读

分享文章

警惕!在try块里关闭流,是典型的资源泄露“元凶”
目录一.先看一眼代码1.存在的问题分析原因二.为什么在try块里close()是“自杀”行为1.分析原因2.“资源泄露”是啥意思3.正确做法在Finally块中进行close操作可以确保资源被关闭4.最佳实践Try-With-Resources三.补充资源有哪些类型哪些需要被close哪些不需要1.资源的类型2.如何判断一个资源需不需要关闭close总结今天经过排查之前写的一段关于文件下载的代码发现了一个非常典型且危险的写法。相信很多刚入行或者对IO流机制理解不深的开发者都踩过这个坑。今天我们就借这个案例彻底聊聊Java中资源关闭的那些事儿。一.先看一眼代码在之前写的一个Spring Boot的接口用于从远程服务获取PDF文件并下载代码逻辑大概是这样的GetMapping(/下载PDF文件) public void down(HttpServletResponse response) { // ...省略参数处理和远程调用... byte[] bytes getRemoteFileBytes(); try { ServletOutputStream outputStream response.getOutputStream(); outputStream.write(bytes); outputStream.flush(); outputStream.close(); // ⚠️ 注意这里的隐患在try块里手动关闭 logger.info(下载成功); } catch (Exception e) { logger.error(下载异常, e); } }乍一看这段代码逻辑通顺获取流 - 写数据 - 刷新 - 关闭。看起来很完美对吧1.存在的问题分析原因但我直接指出了其中的问题你这是在“掩耳盗铃”如果写入过程中报错流永远不会被关闭。说白了在try里关流close操作就是“只有成功代码不报错才会执行一旦try中的close之前的代码报错了根本没机会执行close这样资源一直被占用无法释放”在finally里关流才是“不管成败必须善后因为finally必定会执行即使抛异常执行catch操作后也会执行finally因此在finally里面执行close关闭流就是一个保底操作好赖都会给资源关了”。因此此时我在try中进行close关闭流肯定是大忌所以要改成在finally里面进行close操作。二.为什么在try块里close()是“自杀”行为1.分析原因很多开发者认为只要我写了close()资源就会被释放。但请仔细看代码执行的流程程序执行到outputStream.write(bytes)。假设此时网络抖动或者磁盘满了或者内存溢出了这里抛出了一个IOException。JVM捕获到异常立即跳出当前的try块直接进入catch块。结果outputStream.close()这行代码被无情地跳过了这就导致了严重的资源泄露。在高并发场景下如果频繁发生写入异常服务器的文件句柄File Descriptor或TCP连接数会迅速耗尽最终导致服务崩溃抛出Too many open files错误。2.“资源泄露”是啥意思注意所谓的“资源泄露”就是申请的流没被关闭导致资源一直被占用少量资源被占用还好要是时间长了资源都泄露了资源都被占系统就崩溃了。3.正确做法在Finally块中进行close操作可以确保资源被关闭正确的“手动挡”写法Finally块在Java 7之前我们标准的做法是将资源关闭操作放在finally块中。因为finally块中的代码无论try块中是否发生异常都会被执行。如果要手动管理正确的姿势是这样的ServletOutputStream outputStream null; try { outputStream response.getOutputStream(); outputStream.write(bytes); outputStream.flush(); } catch (Exception e) { logger.error(下载异常, e); // 这里最好还要处理一下响应比如返回500状态码防止前端傻等 } finally { // ✅ 只有在这里才能确保万无一失 if (outputStream ! null) { try { outputStream.close(); } catch (IOException e) { logger.error(关闭流异常, e); } } }虽然这样写很稳健但你会发现代码变得非常臃肿充满了防御性代码判空、嵌套try-catch。4.最佳实践Try-With-Resources从Java 7开始官方引入了try-with-resources语法糖这是目前公认的最优雅的资源管理方式。我们只需要在try后面的括号里声明资源Java编译器会自动帮我们生成finally块来关闭它。优化后的代码// 推荐使用 try-with-resources try (ServletOutputStream outputStream response.getOutputStream()) { byte[] bytes getRemoteFileBytes(); outputStream.write(bytes); outputStream.flush(); // 出了这个花括号JVM会自动、安全地调用 close() } catch (Exception e) { logger.error(下载异常, e); // 异常处理... }为什么它更好自动关闭即使write抛出异常JVM也会确保close()被执行。异常抑制如果在try块中发生异常且在close()时也发生了异常Java会将close()的异常作为“被抑制异常”Suppressed Exception附加在主异常上不会丢失关键错误信息。代码整洁少写了十几行垃圾代码可读性极大提升。特别补充ServletOutputStream的特殊性在写这篇文章时我也查阅了相关资料。对于ServletOutputStream这种由Web容器如Tomcat管理的流其实有一个潜规则通常情况下你甚至不需要手动关闭它。因为Spring MVC或Servlet容器在处理完请求后会自动调用response的销毁方法进而关闭流。如果你手动关闭了它而后续的拦截器或容器本身又尝试向流中写入数据比如错误页面可能会抛出IllegalStateException: Stream is closed。但是这不成为我们在try块中间手动close的理由。如果你不确定容器的行为或者你使用的是普通的文件流FileOutputStream必须关闭。三.补充资源有哪些类型哪些需要被close哪些不需要1.资源的类型资源类别典型类名为什么要关文件流FileInputStream,BufferedReader释放文件句柄否则文件删不掉、打不开。网络流Socket,ServerSocket释放端口/连接否则连不上服务器。数据库Connection,ResultSet归还数据库连接否则数据库连不上。纯内存对象String,ArrayList,HashMap不需要关它们只占内存GC 会自动回收。2.如何判断一个资源需不需要关闭close如果你记不住那么多类有一个最简单的判断标准看它是不是“借”来的。借来的需要关代码里用了new FileInputStream(...)指定了具体的文件名。代码里用了new Socket(..., 8080)指定了具体的IP和端口。代码里用了DriverManager.getConnection(...)连了具体的数据库。特征这些对象背后都对应着操作系统的一个句柄或外部系统的会话必须还。自带的不需要关new String(hello)new ArrayList()特征这些纯粹是内存里的数据没有连接到外部硬件或系统不用还等着 GC 收尸就行。一句话口诀凡是涉及“读写文件”、“联网”、“连数据库”的对象用完必须close()总结永远不要在try块的业务逻辑中间手动调用close()那是掩耳盗铃。如果是老旧项目Java 6及以下请务必使用try-catch-finally模式。现代Java开发请无脑使用try-with-resources既安全又优雅。写代码不仅要能跑通更要考虑“跑不通”的时候会发生什么。希望这个小技巧能帮你避开生产环境的深坑以上就是本篇文章的全部内容喜欢的话可以留个免费的关注呦~~~

更多文章