Утечка ресурсов в Files.list(путь), когда поток явно не закрыт?

Недавно я написал небольшое приложение, которое периодически проверяло содержимое каталога. Через некоторое время приложение разбилось из-за слишком большого количества открытых файлов. После некоторой отладки я обнаружил ошибку в следующей строке:

Files.list(Paths.get(destination)).forEach(path -> {
     // To stuff
});

Затем я проверил javadoc (вероятно, должен был сделать это раньше) для Files.list и нашел:

* <p> The returned stream encapsulates a {@link DirectoryStream}.
* If timely disposal of file system resources is required, the
* {@code try}-with-resources construct should be used to ensure that the
* stream {@link Stream#close close} method is invoked after the stream
* operations are completed

Для меня "своевременное удаление" по-прежнему звучит так, как будто ресурсы будут выпущены в конце концов, прежде чем приложение перестанет работать. Я просмотрел код JDK (1.8.60), но мне не удалось найти подсказки о том, что дескрипторы файлов, открытые Files.list, снова выпущены.

Затем я создал небольшое приложение, которое явным образом вызывает сборщик мусора после использования Files.list следующим образом:

while (true) {
    Files.list(Paths.get("/")).forEach(path -> {
      System.out.println(path);
    });
    Thread.sleep(5000);

    System.gc();
    System.runFinalization();
}

Когда я проверил открытые дескрипторы файлов с помощью lsof -p <pid>, я все равно мог видеть список открытых дескрипторов файлов для "/" дольше и дольше.

Теперь мой вопрос: есть ли скрытый механизм, который должен в конечном итоге закрывать больше не использовать открытые дескрипторы файлов в этом сценарии? Или эти ресурсы на самом деле никогда не расположены, и javadoc немного эвфемистичен, когда говорит о "своевременной утилизации ресурсов файловой системы"?

Ответ 1

Если вы закрываете поток, Files.list() закрывает базовый DirectoryStream, который он использует для потоковой передачи файлов, поэтому утечка ресурсов не должна протекать, пока вы закрываете поток.

Вы можете видеть, где DirectoryStream закрыт в исходном коде для Files.list() здесь:

return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.DISTINCT), false)
                    .onClose(asUncheckedRunnable(ds));

Главное, чтобы понять, что Runnable зарегистрирован в потоке с использованием Stream::onClose, который вызывается, когда сам поток закрыт. Этот Runnable создается с помощью метода factory, asUncheckedRunnable, который создает Runnable, который закрывает переданный в него ресурс, переводя любой IOException, созданный во время close(), на UncheckedIOException

Вы можете с уверенностью уверять, что DirectoryStream закрыт, закрыв Stream следующим образом:

try (Stream<Path> files = Files.list(Paths.get(destination))){
    files.forEach(path -> {
         // Do stuff
    });
}

Ответ 2

Что касается части IDE: Eclipse выполняет анализ утечки ресурсов на основе локальных переменных (и явных выражений выделения ресурсов), поэтому вам нужно только извлечь поток в локальную переменную:

Stream<Path> files =Files.list(Paths.get(destination));
files.forEach(path -> {
 // To stuff
});

Затем Eclipse расскажет вам

утечка ресурса: "файлы" никогда не закрываются

За кулисами анализ работает с каскадом исключений:

  • Все Closeable необходимо закрыть
  • java.util.stream.Stream (который является Closeable) закрывает не
  • Все потоки, создаваемые методами java.nio.file.Files do, нуждаются в закрытии

Эта стратегия была разработана в координации с библиотечной командой, когда они обсуждали, должно ли Stream быть AutoCloseable.

Ответ 3

   List<String> fileList = null;

   try (Stream<Path> list = Files.list(Paths.get(path.toString()))) {
   fileList = 
     list.filter(Files::isRegularFile).map(Path::toFile).map(File::getAbsolutePath)
                                .collect(Collectors.toList());
   } catch (IOException e) {
    logger.error("Error occurred while reading email files: ", e);
  }