前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:https://www.captainbed.cn/z
文章目录
1. 错误场景复现
场景1:未关闭的IO流导致文件句柄耗尽
// 读取大文件时忘记关闭流
public void readFile(String path) {
try {
FileInputStream fis = new FileInputStream(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
// 忘记调用 fis.close() 和 reader.close()
} catch (IOException e) {
e.printStackTrace();
}
}
后果:连续调用后触发Too many open files
系统错误,导致进程崩溃。
场景2:数据库连接泄漏
// 未正确关闭数据库连接
public List<User> queryUsers() {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapRow(rs));
}
// 忘记关闭 rs、stmt、conn
return users;
}
后果:数据库连接池被占满,新请求因获取不到连接而超时。
场景3:异常分支导致关闭遗漏
// 异常导致close()未执行
public void copyFile(String src, String dest) {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(src);
out = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
log.error("复制失败");
} finally {
if (in != null) in.close(); // 若out.close()抛异常,in.close()不会执行!
if (out != null) out.close();
}
}
隐患:若out.close()
抛出异常,in.close()
将被跳过。
2. 原理解析
资源泄漏的底层危害
-
文件描述符泄漏:
- 操作系统对进程打开文件数有限制(默认1024)
- 泄漏导致后续文件操作失败(
java.io.IOException: Too many open files
)
-
数据库连接池耗尽:
- 连接池中的连接未被归还,新请求排队等待
- 典型症状:
ConnectionTimeoutException
-
内存泄漏:
- 未关闭的资源对象持有底层系统资源,无法被GC回收
为什么GC无法拯救资源泄漏?
- GC只管理堆内存:资源句柄(如文件描述符、Socket端口)由操作系统管理
- Finalizer不可靠:
finalize()
方法执行时机不确定,不能作为关闭资源的保障
3. 正确解决方案
方案1:try-with-resources(Java 7+)
// 自动关闭所有实现了AutoCloseable的资源
public void safeReadFile(String path) {
try (FileInputStream fis = new FileInputStream(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
} catch (IOException e) {
// 异常处理
}
// 无需手动调用close()
}
执行顺序:
- 资源按照声明逆序关闭(先
reader
后fis
) - 关闭时的异常会被压制,可通过
getSuppressed()
获取
方案2:连接池的正确使用姿势
// 使用标准数据源管理连接
public List<User> queryUsers() {
try (Connection conn = dataSource.getConnection(); // 连接池托管
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapRow(rs));
}
return users;
} // 自动关闭顺序:ResultSet → Statement → Connection(归还连接池)
}
连接池配置关键参数:
spring.datasource:
hikari:
maximum-pool-size: 10 # 最大连接数
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
connection-timeout: 2000 # 获取连接超时时间
leak-detection-threshold: 5000 # 泄漏检测阈值
方案3:防御性关闭工具类
// Apache Commons IO提供的安全关闭方法
public static void closeQuietly(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
// 静默处理(通常用于finally块)
}
}
// 改造旧代码
finally {
IOUtils.closeQuietly(in); // 处理可能出现的异常
IOUtils.closeQuietly(out);
}
注意:Java 7+项目应优先使用try-with-resources,此方案适用于兼容旧代码。
4. 工具与最佳实践
资源泄漏检测工具
-
JDK内置监控:
# 查看进程文件描述符使用量 lsof -p <pid> | wc -l
-
连接池监控:
- HikariCP的
HikariPoolMXBean
- Druid的监控页面
- HikariCP的
-
VisualVM插件:
- 安装
VisualGC
插件,观察堆外内存使用情况
- 安装
代码规范建议
- 禁止直接调用资源类:统一通过连接池/工具类获取资源
- 静态代码扫描规则:
<!-- SpotBugs规则 --> <Match> <Bug category="BAD_PRACTICE" pattern="OS_OPEN_STREAM" /> </Match>
- 日志增强:记录资源打开/关闭的日志(调试阶段启用)
5. Code Review检查清单
检查项 | 正确做法 |
---|---|
是否使用try-with-resources? | Java 7+项目必须使用 |
连接池配置是否合理? | 检查最大连接数、超时时间、泄漏检测阈值 |
finally块是否安全关闭资源? | 每个close()调用单独try-catch,或使用工具类 |
是否处理关闭时的异常? | 至少记录日志,避免掩盖原始错误 |
6. 真实案例
某票务系统在高峰期出现服务不可用:
- 现象:数据库连接池达到最大值,所有请求超时
- 分析:发现历史代码中某处查询未关闭
ResultSet
,连接未被归还 - 修复:
- 使用try-with-resources重构所有数据库操作
- 配置Druid连接池的
removeAbandonedTimeout
参数自动回收泄漏连接 - 增加连接池监控大盘和报警规则
结果:系统连续平稳运行30天无连接池耗尽事件。
总结
- 资源关闭是义务:必须使用try-with-resructures或finally块
- 连接池不是银弹:错误使用仍会导致泄漏,需配合监控
- 工具决定效率:静态扫描 + 运行时监控 = 双重防护
- 防御性编程:总是假设资源可能泄漏,提前建立防线
下期预告:《异常处理三宗罪:吞异常、过度捕获与日志滥用》——从生产环境血泪案例解析异常处理的最佳实践。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集