为什么使用java.nio.files.File :: list会导致这个广度优先的文件遍历程序因"Too many open files"错误而崩溃?

Lam*_*bda 3 java nio java-io

假设:

Streams是惰性的,因此以下语句不会加载path内存引用的目录的整个子节点; 相反,它逐个加载它们,并且在每次调用之后forEach,引用的目录p都有资格进行垃圾收集,因此它的文件描述符也应该关闭:

Files.list(path).forEach(p -> 
   absoluteFileNameQueue.add(
      p.toAbsolutePath().toString()
   )
);
Run Code Online (Sandbox Code Playgroud)

基于这个假设,我实现了广度优先的文件遍历工具:

public class FileSystemTraverser {

    public void traverse(String path) throws IOException {
        traverse(Paths.get(path));
    }

    public void traverse(Path root) throws IOException {
        final Queue<String> absoluteFileNameQueue = new ArrayDeque<>();
        absoluteFileNameQueue.add(root.toAbsolutePath().toString());

        int maxSize = 0;
        int count = 0;

        while (!absoluteFileNameQueue.isEmpty()) {
            maxSize = max(maxSize, absoluteFileNameQueue.size());
            count += 1;
            Path path = Paths.get(absoluteFileNameQueue.poll());

            if (Files.isDirectory(path)) {
                Files.list(path).forEach(p ->
                        absoluteFileNameQueue.add(
                                p.toAbsolutePath().toString()
                        )
                );
            }

            if (count % 10_000 == 0) {
                System.out.println("maxSize = " + maxSize);
                System.out.println("count = " + count);
            }
        }

        System.out.println("maxSize = " + maxSize);
        System.out.println("count = " + count);
    }

}
Run Code Online (Sandbox Code Playgroud)

我以一种相当直接的方式使用它:

public class App {

    public static void main(String[] args) throws IOException {
        FileSystemTraverser traverser = new FileSystemTraverser();
        traverser.traverse("/media/Backup");
    }

}
Run Code Online (Sandbox Code Playgroud)

安装的磁盘/media/Backup有大约300万个文件.

由于某种原因,大约140,000标记,程序崩溃与此堆栈跟踪:

Exception in thread "main" java.nio.file.FileSystemException: /media/Backup/Disk Images/Library/Containers/com.apple.photos.VideoConversionService/Data/Documents: Too many open files
    at sun.nio.fs.UnixException.translateToIOException(UnixException.java:91)
    at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
    at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
    at sun.nio.fs.UnixFileSystemProvider.newDirectoryStream(UnixFileSystemProvider.java:427)
    at java.nio.file.Files.newDirectoryStream(Files.java:457)
    at java.nio.file.Files.list(Files.java:3451)
Run Code Online (Sandbox Code Playgroud)

在我看来,由于某种原因文件描述符没有被关闭或Path对象不是垃圾收集导致应用程序最终崩溃.

系统细节

  • 操作系统:是Ubuntu 15.0.4
  • 内核:4.4.0-28-通用
  • ulimit:无限制
  • 文件系统:btrfs
  • Java运行时:使用OpenJDK 1.8.0_91和Oracle JDK 1.8.0_91进行测试

任何想法我在这里缺少什么,我如何解决这个问题(没有诉诸java.io.File::list(即通过留在NIO2和Paths 的令人满意)?


更新1:

我怀疑JVM是否保持文件描述符处于打开状态.我把这个堆转储带到120,000个文件标记周围:

VisualVM堆转储

更新2:

我在VisualVM中安装了一个文件描述符探测插件,实际上它显示FD没有被处理掉(正如cerebrotecnologico和k5正确指出的那样):

VisualVM文件描述符跟踪

k5_*_*k5_ 7

好像从Files.list(Path)返回的Stream没有正确关闭.另外,你不应该在流上使用forEach,你不确定它不是并行的(因此.sequential()).

    try (Stream<Path> stream = Files.list(path)) {
        stream.map(p -> p.toAbsolutePath().toString()).sequential().forEach(absoluteFileNameQueue::add);
    }
Run Code Online (Sandbox Code Playgroud)


cer*_*ico 6

从Java文档:

"返回的流封装了DirectoryStream.如果需要及时处理文件系统资源,则应使用try-with-resources构造来确保在流操作完成后调用流的close方法"