IO 资源泄漏问题排查
你有没有遇到过这种情况:程序跑着跑着,突然报「Too many open files」?
这不是玄学,这是资源泄漏。
本节讲清楚 IO 资源泄漏的原因、常见场景,以及排查工具。
资源泄漏的本质
每个打开的文件、Socket 连接、数据库连接,都会占用一个文件描述符(file descriptor)。
操作系统的文件描述符数量是有限的(Linux 默认 1024)。如果泄漏了,耗尽后就再也打不开新文件了。
最常见的泄漏:流未关闭
java
// ❌ 异常时流未关闭
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// IO 操作
fis.read();
// 如果这里抛异常,流泄漏!
} finally {
if (fis != null) fis.close(); // 正常路径会执行
}
// ✅ try-with-resources:自动关闭,异常也关闭
try (FileInputStream fis = new FileInputStream("data.txt")) {
fis.read();
} // 自动关闭关键点:try-with-resources 在 try 块退出时(包括异常退出)都会调用 close()。
嵌套流只需关外层
java
// ❌ 多此一举
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
bis.read();
fis.close(); // 不需要!bis.close() 会自动关闭 fis
}
// ✅ 只需要关最外层
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.txt"))) {
bis.read();
}常见泄漏场景
场景 1:未关闭 Socket 连接
java
// ❌ Socket 连接泄漏
Socket socket = new Socket("localhost", 8080);
InputStream in = socket.getInputStream();
// 如果代码抛出异常或提前返回,socket 永远不会 close
socket.close();
// ✅ try-with-resources
try (Socket socket = new Socket("localhost", 8080)) {
// IO 操作
} // 自动关闭 socket 和底层流场景 2:缓存未 flush
java
// ❌ 数据未 flush 到磁盘
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("out.txt"));
bos.write("data".getBytes());
// 程序崩溃,最后的 "data" 可能还在缓冲区
bos.close(); // close 时自动 flush,但崩溃了就来不及了
// ✅ 或手动 flush
bos.flush();场景 3:循环中创建流未关闭
java
// ❌ 循环中创建流,每次都泄漏
for (String file : files) {
FileInputStream fis = new FileInputStream(file);
// 处理文件
// 忘记关闭!
}
// ✅ 循环中使用 try-with-resources
for (String file : files) {
try (FileInputStream fis = new FileInputStream(file)) {
// 处理文件
} // 自动关闭
}排查工具
Linux/macOS:lsof 查看打开的文件
bash
# 查看进程打开的文件描述符数量
lsof -p <pid> | wc -l
# 查看打开了哪些文件
lsof -p <pid>
# 查找泄漏的文件句柄(按类型过滤)
lsof -p <pid> | grep -E "REG|TYPE" | head -20JVM 命令:jcmd 查看
bash
# 查看打开的描述符
jcmd <pid> openDescriptors.object
# 查看类加载历史(辅助分析)
jcmd <pid> GC.class_histogram | grep -i fileArthas 监控
bash
# 启动 Arthas
java -jar arthas-boot.jar
# 查看打开的文件描述符
$ dashboard -b
# 查看具体线程持有的资源
$ thread -n 5代码级排查
java
// 在关键位置打印资源状态(调试用)
public class ResourceTracker {
private static final Logger log = LoggerFactory.getLogger(ResourceTracker.class);
private static final Set<Object> openResources = ConcurrentHashMap.newKeySet();
public static <T extends AutoCloseable> T track(T resource, String name) {
openResources.add(resource);
log.debug("Opened: {} (total: {})", name, openResources.size());
return resource;
}
public static void closeAndUntrack(AutoCloseable resource) throws Exception {
resource.close();
openResources.remove(resource);
log.debug("Closed (total: {})", openResources.size());
}
public static int getOpenCount() {
return openResources.size();
}
}预防措施
- 永远用 try-with-resources:不只是 IO 流,Socket、数据库连接、线程池都一样
- 开启 IDE 警告:大多数 IDE 能检测到未关闭的资源
- 代码审查:注意循环中的资源创建
- 监控告警:设置文件描述符数量的监控阈值
记住这个教训:
资源泄漏是慢性毒药,平时没事,一出问题就是大事。 永远用 try-with-resources,永远。
