Как вывести список 2 миллионов файлов в каталоге Java без исключения «недостаточно памяти» - PullRequest
20 голосов
/ 29 июня 2010

Мне приходится иметь дело с каталогом из примерно 2 миллионов xml, которые нужно обработать.

Я уже решил обработку, распределяющую работу между машинами и потоками с использованием очередей, и все идет хорошо.

Но сейчас большая проблема - узкое место чтения каталога с 2 миллионами файлов для постепенного заполнения очередей.

Я пытался использовать метод File.listFiles(), но он дает исключение java out of memory: heap space. Есть идеи?

Ответы [ 15 ]

11 голосов
/ 29 июня 2010

Прежде всего, есть ли у вас возможность использовать Java 7? Там у вас есть FileVisitor и Files.walkFileTree, которые, вероятно, должны работать в рамках ваших ограничений памяти.

В противном случае я могу думать только о том, чтобы использовать File.listFiles(FileFilter filter) с фильтром, который всегда возвращает false (гарантируя, что полный массив файлов никогда не будет храниться в памяти), но который ловит файлы. обрабатываться по пути и, возможно, помещать их в очередь производителя / потребителя или записывать имена файлов на диск для последующего обхода.

В качестве альтернативы, если вы управляете именами файлов или если они именуются каким-либо приятным способом, вы можете обрабатывать файлы в чанках, используя фильтр, который принимает имена файлов в форме file0000000 - filefile0001000 затем file0001000 - filefile0002000 и т. Д.

Если имена не названы по-хорошему, вы можете попробовать отфильтровать их по хеш-коду имени файла, который должен быть равномерно распределен по набору целых чисел.


Обновление: Вздох. Вероятно, не будет работать. Только что взглянул на реализацию listFiles:

public File[] listFiles(FilenameFilter filter) {
    String ss[] = list();
    if (ss == null) return null;
    ArrayList v = new ArrayList();
    for (int i = 0 ; i < ss.length ; i++) {
        if ((filter == null) || filter.accept(this, ss[i])) {
            v.add(new File(ss[i], this));
        }
    }
    return (File[])(v.toArray(new File[v.size()]));
}

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

Кстати, не могли бы вы привести пример имени файла? Они "догадываются"? Как

for (int i = 0; i < 100000; i++)
    tryToOpen(String.format("file%05d", i))
9 голосов
/ 29 июня 2010

Если Java 7 не поддерживается, этот хак будет работать (для UNIX):

Process process = Runtime.getRuntime().exec(new String[]{"ls", "-f", "/path"});
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while (null != (line = reader.readLine())) {
    if (line.startsWith("."))
        continue;
    System.out.println(line);
}

Параметр -f ускорит его (с man ls):

-f     do not sort, enable -aU, disable -lst
8 голосов
/ 29 июня 2010

Используйте File.list() вместо File.listFiles() - возвращаемые им объекты String занимают меньше памяти, чем объекты File, и (что более важно, в зависимости от расположения каталога) они не содержит полного пути.

Затем создайте File объектов по мере необходимости при обработке результата.

Однако это также не будет работать для произвольно больших каталогов. В целом лучше организовать файлы в иерархию каталогов, чтобы ни в одном каталоге не было более нескольких тысяч записей.

5 голосов
/ 09 января 2013

Если вы можете использовать Java 7, это можно сделать таким образом, и у вас не будет проблем с нехваткой памяти.

Path path = FileSystems.getDefault().getPath("C:\\path\\with\\lots\\of\\files");
        Files.walkFileTree(path, new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                // here you have the files to process
                System.out.println(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
               return FileVisitResult.TERMINATE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
              return FileVisitResult.CONTINUE;
            }
        });
3 голосов
/ 24 июля 2013

Вы можете сделать это с помощью библиотеки Apache FileUtils.Нет проблем с памятью.Я проверил с visualvm.

  Iterator<File> it = FileUtils.iterateFiles(folder, null, true);
  while (it.hasNext())
  {
     File fileEntry = (File) it.next();
  }

Надеюсь, это поможет.прощай

2 голосов
/ 17 июля 2011

Поскольку вы работаете в Windows, похоже, что вам просто нужно было использовать ProcessBuilder для запуска чего-то вроде «cmd / k dir / b target_directory», записать вывод этого и направить его в файл.Затем вы можете обрабатывать этот файл за раз, считывая имена файлов и обрабатывая их.

Лучше поздно, чем никогда?;)

1 голос
/ 01 ноября 2017

Это также требует Java 7, но это проще, чем ответ Files.walkFileTree, если вы просто хотите перечислить содержимое каталога, а не обходить все дерево:

Path dir = Paths.get("/some/directory");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path path : stream) {
        handleFile(path.toFile());
    }
} catch (IOException e) {
    handleException(e);
}

Реализация DirectoryStream зависит от платформы и никогда не вызывает File.list или что-либо подобное, вместо этого используются системные вызовы Unix или Windows, которые перебирают каталог по одной записи за раз.

1 голос
/ 29 июня 2010

Почему в любом случае вы храните 2 миллиона файлов в одном каталоге?Я могу себе представить, что это ужасно замедляет доступ уже на уровне ОС.

Я бы определенно хотел разделить их на подкаталоги (например, по дате / времени создания) уже перед обработкой.Но если по какой-то причине это невозможно, можно ли это сделать во время обработки?Например, переместите 1000 файлов, поставленных в очередь для Process1, в Directory1, еще 1000 файлов для Process2 в Directory2 и т. Д. Затем каждый процесс / поток видит только (ограниченное количество) файлов, распределенных для него.

0 голосов
/ 25 ноября 2016

Я столкнулся с той же проблемой, когда разрабатывал приложение для сканирования вредоносных программ.Мое решение - выполнить команду оболочки, чтобы вывести список всех файлов.Это быстрее, чем рекурсивные методы, чтобы просмотреть папку за папкой.

Подробнее о команде оболочки читайте здесь: http://adbshell.com/commands/adb-shell-ls

        Process process = Runtime.getRuntime().exec("ls -R /");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));

        //TODO: Read the stream to get a list of file path.
0 голосов
/ 28 сентября 2015

В качестве первого подхода вы можете попробовать настроить некоторые параметры памяти JVM, например, увеличьте размер кучи, как это было предложено, или даже используйте параметр AggressiveHeap. Учитывая большое количество файлов, это может не помочь, тогда я бы предложил обойти проблему. Создайте несколько файлов с именами файлов в каждом, скажем, 500k имен файлов на файл и прочитайте их.

...