DO NOT 忘记关闭文件流

johnniang 2019年11月23日 466次浏览

package java.nio.file 包下有一个文件操作的工具类:java.nio.file.Files (Java 8 及以上),在使用此工具类的时候,强烈建议详细看看 API 文档,否则可能造成“病毒式”的隐患。

案例

最近操作文件的代码中经常使用 Files.list,但莫名其妙地出现了 Too many open files 这样的错误,错误详情如下:

2019-11-19 09:05:01.329 ERROR 24651 --- [XNIO-1 Accept] org.xnio.nio.tcp.server: Exception accepting request, closing server channel TCP server (NIO) <2fa63774>

java.io.IOException: Too many open files
        at java.base/sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) ~[na:na]
        at java.base/sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:525) ~[na:na]
        at java.base/sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:277) ~[na:na]
        at org.xnio.nio.QueuedNioTcpServer.handleReady(QueuedNioTcpServer.java:467) ~[xnio-nio-3.3.8.Final.jar!/:3.3.8.Final]
        at org.xnio.nio.QueuedNioTcpServerHandle.handleReady(QueuedNioTcpServerHandle.java:38) ~[xnio-nio-3.3.8.Final.jar!/:3.3.8.Final]
        at org.xnio.nio.WorkerThread.run(WorkerThread.java:561) ~[xnio-nio-3.3.8.Final.jar!/:3.3.8.Final]

直接导致了 Web 服务的端口监听阵亡,系统无法接收到外部请求。

虽然可以通过调整 Linuxopen files limit 解除这个限制,但是隐患依然潜伏在系统中。

真相

在日志文件中发现最接近真相的日志:

Caused by: java.nio.file.FileSystemException: /tmp/halo-backup: Too many open files
        at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:100) ~[na:na]
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111) ~[na:na]
        at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116) ~[na:na]
        at java.base/sun.nio.fs.UnixFileSystemProvider.newDirectoryStream(UnixFileSystemProvider.java:432) ~[na:na]
        at java.base/java.nio.file.Files.newDirectoryStream(Files.java:474) ~[na:na]
        at java.base/java.nio.file.Files.list(Files.java:3769) ~[na:na]
        at run.halo.app.service.impl.BackupServiceImpl.listHaloBackups(BackupServiceImpl.java:172) ~[classes!/:1.1.3-beta.1]

从上面的日志分析,极有可能是操作文件的时候忘记关闭文件流而导致的错误。通过不断复现,最后确认问题所在:调用 Files.list 未手动关闭文件流。

Files.list 返回类型是 Stream<Path>,通常我们会这样使用:

List<Path> paths = Files.list(Paths.get("Path-of-Folder"))
                    .collect(Collectors.toList());

悲剧就发生在此!该方法所操作的所有文件都将处于打开状态,文件操作比较少的场景下不太容易复现以发现问题。

解决

查询 Files.list API 文档后,发现作者留下的 API 使用提示:

@apiNote
This method must be used within a try-with-resources statement or similar control structure to ensure that the stream's open directories are closed promptly after the stream's operations have completed.

该方法必须用 try-with-resources 语句包裹或者类似这样的机制来保证关闭该流所打开的目录。

  1. 尝试通过 try-with-resources 包裹解决
try (Stream<Path> pathStream = Files.list(Paths.get("Path-of-Folder"))) {
        List<Path> paths = pathStream.collect(Collectors.toList());
}
  1. 尝试通过 try-finally 解决
Stream<Path> pathStream = null;
try {
    pathStream = Files.list(Paths.get("Path-of-Folder"));
    List<Path> paths = pathStream.collect(Collectors.toList());
} finally {
    if (pathStream != null) {
        pathStream.close();
    }
}

提醒

永远不要忘记关闭 Files.walk 或者 Files.list 返回的 Stream<Path>