Skip to content

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 -20

JVM 命令:jcmd 查看

bash
# 查看打开的描述符
jcmd <pid> openDescriptors.object

# 查看类加载历史(辅助分析)
jcmd <pid> GC.class_histogram | grep -i file

Arthas 监控

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();
    }
}

预防措施

  1. 永远用 try-with-resources:不只是 IO 流,Socket、数据库连接、线程池都一样
  2. 开启 IDE 警告:大多数 IDE 能检测到未关闭的资源
  3. 代码审查:注意循环中的资源创建
  4. 监控告警:设置文件描述符数量的监控阈值

记住这个教训

资源泄漏是慢性毒药,平时没事,一出问题就是大事。 永远用 try-with-resources,永远。

基于 VitePress 构建