在 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 服务的端口监听阵亡,系统无法接收到外部请求。
虽然可以通过调整 Linux
的 open 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
语句包裹或者类似这样的机制来保证关闭该流所打开的目录。
- 尝试通过
try-with-resources
包裹解决
try (Stream<Path> pathStream = Files.list(Paths.get("Path-of-Folder"))) {
List<Path> paths = pathStream.collect(Collectors.toList());
}
- 尝试通过
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>
!