Почему чтение файла в память занимает 4 раза в памяти на Java?

У меня есть следующий код, который читается в следующем файле, добавляет \r\n в конец каждой строки и помещает результат в строковый буфер:

public InputStream getInputStream() throws Exception {
    StringBuffer holder = new StringBuffer();
    try{
        FileInputStream reader = new FileInputStream(inputPath);


        BufferedReader br = new BufferedReader(new InputStreamReader(reader));
        String strLine;
        //Read File Line By Line
        boolean start = true;
        while ((strLine = br.readLine()) != null)   {
            if( !start )    
                holder.append("\r\n");

            holder.append(strLine);
            start = false;
        }
        //Close the input stream
        reader.close();
    }catch (Throwable e){//this is where the heap error is caught up to 2Gb
      System.err.println("Error: " + e.getMessage());
    }


    return new StringBufferInputStream(holder.toString());
}

Я пробовал читать в файле 400 Мб, и я изменил максимальное пространство кучи на 2Gb, и все же он по-прежнему выдает исключение из кучи памяти. Любые идеи?

Ответ 1

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

Ответ 2

Это может быть связано с изменением размера StringBuffer при достижении емкости. Это связано с созданием нового char[] двойного размера предыдущего и последующего копирования содержимого в новый массив. Вместе с моментами, которые уже были написаны о символах на Java, которые хранятся как 2 байта, это, безусловно, добавит к использованию вашей памяти.

Чтобы решить эту проблему, вы можете создать StringBuffer с достаточной емкостью для начала, учитывая, что вы знаете размер файла (и, следовательно, приблизительное количество символов для чтения). Однако следует предупредить, что распределение массива также произойдет, если вы затем попытаетесь преобразовать этот большой StringBuffer в String.

Другой момент: обычно вы предпочитаете StringBuilder over StringBuffer, поскольку операции с ним быстрее.

Вы можете рассмотреть возможность внедрения своего собственного "CharBuffer", используя, например, LinkedList из char [], чтобы избежать дорогостоящих операций по распределению/копированию массивов. Вы можете реализовать этот класс CharSequence и, возможно, полностью отказаться от преобразования в String. Еще одно предложение для более компактного представления: если вы читаете текст на английском языке, содержащий большое количество повторяющихся слов, вы можете читать и хранить каждое слово, используя функцию String.intern(), чтобы значительно сократить объем хранилища.

Ответ 3

Для начала строки Java - это UTF-16 (т.е. 2 байта на символ), поэтому, если ваш входной файл является ASCII или похожим однобайтовым символом, тогда holder будет ~ 2x размер входные данные плюс дополнительный \r\n на строку и любые дополнительные накладные расходы. Там ~ 800 МБ сразу, предполагая очень низкую накладную память в StringBuffer.

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

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

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

Ответ 4

У вас есть ряд проблем:

  • Unicode: символы занимают в два раза больше места в памяти, чем на диске (при условии кодирования 1 байт)
  • Изменение размера StringBuffer: может удвоить (постоянно) и утроить (временно) занятую память, хотя это наихудший случай
  • StringBuffer.toString() временно удваивает занятую память, так как она делает копию

Все эти сочетания означают, что вам может потребоваться временно до 8-кратного размера вашего файла в ОЗУ, т.е. 3.2G для файла 400M. Даже если ваша машина физически имеет столько оперативной памяти, она должна работать на 64-битной ОС и JVM, чтобы на самом деле получить эту кучу для JVM.

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

Ответ 5

Это StringBuffer. Пустой конструктор создает StringBuffer с начальной длиной 16 байтов. Теперь, если вы добавите что-то, а емкость не достаточна, он добавит Arraycopy из внутреннего массива String в новый буфер.

Таким образом, на самом деле, с каждой добавленной строкой StringBuffer должен создать копию полного внутреннего массива, который почти удваивает требуемую память при добавлении последней строки. Вместе с представлением UTF-16 это приводит к наблюдаемому требованию памяти.

Edit

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

В любом случае, я узнал урок: StringBuffer (и Builder) может вызвать неожиданные ошибки OutOfMemory, и я всегда буду инициализировать его размером, по крайней мере, когда мне нужно хранить большие строки. Спасибо за вопрос:)

Ответ 6

При последней вставке в StringBuffer вам нужно в три раза больше выделенной памяти, потому что StringBuffer всегда расширяется (размером + 1) * 2 (что уже удваивается из-за юникода). Таким образом, файл 400 ГБ может потребовать выделения 800 ГБ * 3 == 2,4 ГБ в конце вставок. Это может быть что-то меньшее, что зависит от того, когда достигается порог.

Предложение об объединении строк вместо использования буфера или строителя здесь. Будет много сбора мусора и создания объекта (поэтому он будет медленным), но гораздо меньший объем памяти.

[При запросе Майкла я исследовал это дальше, и concat здесь не помог, поскольку он копирует буфер char, поэтому, хотя он не потребует тройной, для этого потребуется двойная память в конце.]

Если вы знаете максимальный размер файла и инициализируете размер буфера при создании, вы можете продолжать использовать Buffer (или, еще лучше, Builder), и вы уверены, что этот метод будет вызван только из одного потока в то время.

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

Ответ 7

Я бы предложил использовать кеш-память ОС вместо копирования данных в память Java через символы и обратно в байты. Если вы перечитаете файл по мере необходимости (возможно, измените его, как вы идете), он будет быстрее и, скорее всего, будет проще

Вам нужно более 2 Гбайт, потому что 1-байтные буквы используют char (2-байты) в памяти, и когда ваш размер StringBuffer изменяется, вам нужно удвоить его (чтобы скопировать старый массив в более крупный новый массив). Новый массив обычно составляет 50 % больше, поэтому вам нужно до 6 раз размер оригинального файла. Если производительность не была достаточно плоха, вы используете StringBuffer вместо StringBuilder, который синхронизирует каждый вызов, когда он явно не нужен. (Это только замедляет работу, но использует тот же объем памяти)

Ответ 8

Другие объяснили, почему у вас заканчивается память. Что касается решения этой проблемы, я бы предложил написать собственный подкласс FilterInputStream. Этот класс будет считывать по одной строке за раз, добавлять символы "\ r\n" и буферировать результат. После того, как строка была прочитана потребителем вашего FilterInputStream, вы прочтете другую строку. Таким образом, у вас будет только одна строка в памяти за раз.

Ответ 9

Я также рекомендую проверить класс Commons IO FileUtils для этого. В частности: org.apache.commons.io.FileUtils # readFileToString. Вы также можете указать кодировку, если знаете, что используете только ASCII.