Почему чтение файла в память требует в 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 МБ и изменил максимальное пространство кучи на 2 ГБ, но все равно выдает исключение из кучи памяти. Любые идеи?


person erotsppa    schedule 06.07.2009    source источник
comment
если вы просто пытаетесь преобразовать файл из формата unix в формат windows, я предлагаю вам использовать команду unix2dos, которая доступна в ряде мест (стандартная для большинства Linux, включенная в cygwin и т. д.)   -  person rmeador    schedule 07.07.2009
comment
Потоковое преобразование по-прежнему возможно с использованием java, просто не объединяйте strLine в держатель, а мгновенно печатайте его в FileOutputStream. Можете ли вы показать нам, куда указывает MemExc?   -  person akarnokd    schedule 09.07.2009


Ответы (9)


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

person Chris W. Rea    schedule 06.07.2009
comment
Я удивлен, что меня проголосовали за этот ответ. Действительно, иногда мы, разработчики, тратим время на то, чтобы понять, почему определенный способ ведения дел не работает так, как мы надеялись, когда нам, возможно, следует сделать шаг назад и попробовать другой подход. Я думаю, что каждый раз, когда вы имеете дело с очень большими файлами и загружаете все это в память, первый вопрос должен быть: почему? - person Chris W. Rea; 07.07.2009
comment
Когда разработчик запрашивает решение, очевидно, на то есть причина. Не думайте, что каждый заданный вопрос исходит от старшеклассника. - person erotsppa; 07.07.2009
comment
@erotsppa: Итак... в чем причина? - person Andy Mikula; 07.07.2009
comment
@erotsppa: Согласен. Вот почему я спросил, почему нет, вместо того, чтобы настоятельно заявить, что вы должны. Я сам задавался вопросом, почему не рассматривался другой подход. Не думайте, что каждый ответ снисходителен :-) - person Chris W. Rea; 07.07.2009
comment
Вам не обязательно быть старшеклассником, чтобы увязнуть в деталях и упустить общую картину/альтернативные решения. - person Andrew Coleson; 07.07.2009
comment
@Andreas_D: Не согласен. Может решить проблему, не отвечая прямо на вопрос. Часто вопрос - это проблема! - person Chris W. Rea; 07.07.2009
comment
@Andreas_D: Категорически не согласен. Я думаю, что ответ cwrea действителен, и ваш голос против должен быть аннулирован. - person duffymo; 07.07.2009
comment
Вы можете поспорить, что ответ cwrea не проголосовал бы против, если бы Джон Скит опубликовал то же самое. - person duffymo; 07.07.2009
comment
@duffymo: Нет, пожалуйста, не отменяйте отзыв ... Я с удовольствием принимаю некоторую критику, это часть того, что заставляет сообщество работать :-) - person Chris W. Rea; 07.07.2009
comment
Я думаю, что это правильное замечание, но оно не предлагает большой / никакой помощи, и поэтому 6 голосов кажутся довольно чрезмерными. - person Adamski; 07.07.2009
comment
@Andreas_D: Зависит от того, рассматриваете ли вы вопрос, почему именно я вижу это исключение, а не то, как я могу избежать этого исключения. Если вопрос последний, то полезен ответ, предлагающий перепроектировать программу, чтобы избежать большого потребления памяти. Предложение объяснений внутреннего устройства Java не поможет OP с тем фактом, что независимо от того, что они микро-настраивают, фундаментальный подход к загрузке файла в память имеет плохую сторону. эффект: программа не будет масштабироваться и в конце концов упрется в стену, несмотря на любую микронастройку. - person Chris W. Rea; 07.07.2009
comment
Это не ответ, а очень полезный комментарий. Он должен находиться в разделе комментариев, а не в разделе ответов, и за него не следует голосовать (поскольку он не касается вопроса) bit .ly/MohSi - person OscarRyz; 07.07.2009
comment
@cwrea: я бы сказал, что трудно судить, является ли этот подход в корне неправильным (и что программа упрется в стену), не зная больше о приложении. Возможно, приложение читает/хранит в памяти только один файл, на хост-компьютере может быть 256 ГБ памяти, размер файла никогда не будет превышать X и т. д. - person Adamski; 07.07.2009
comment
@Adamski: Согласен, поэтому я снова спросил, почему бы и нет [...]? Я не просто сформулировал свой ответ в форме вопроса, потому что слишком много смотрел Jeopardy! :-) - person Chris W. Rea; 07.07.2009
comment
Посмотрите на возвращаемое методом значение - подход в корне неверный почти со 100% уверенностью и это единственный разумный ответ. - person Michael Borgwardt; 07.07.2009
comment
Ответ касается проблемы, возможно, не конкретного вопроса, но кого волнует, решает ли он проблему? Что касается этого, то это не вопрос ТАКОЙ чепухи, ну, видимо, ТАК пользователи не согласны, поскольку это ответ с наибольшим количеством голосов. - person Ed S.; 07.07.2009
comment
Хотя я думаю, что ответы предлагают другой подход к проблеме... Я думаю, что часто бывает полезно ответить на реальный вопрос. Гораздо лучше понять, почему один подход лучше другого, чем просто использовать другой подход, потому что он работает. Я думаю, что ОП, возможно, придется рассмотреть другой дизайн, но в основном он пытается понять, что такое память в java. Уроки из опубликованного кода окажутся полезными в будущем. Я не думаю, что такие ответы совершенно неуместны, но я, конечно, надеюсь, что это не станет общепринятым ответом. @ Эд Свангрен: больше нет :-). - person Tom; 07.07.2009
comment
Возможно, он не станет окончательно принятым или лучшим ответом, но он будет иметь наибольшее количество комментариев, LOL! - person Chris W. Rea; 07.07.2009
comment
Позвольте мне сказать это так; если я задаю вопрос, а какой-нибудь пользователь SO говорит: «Эй, ты все делаешь неправильно, попробуй это!», и я делаю, и это отлично работает, я счастлив. - person Ed S.; 07.07.2009

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

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

Еще один момент: обычно вам следует отдавать предпочтение StringBuilder, а не StringBuffer, поскольку операции с ним выполняются быстрее.

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

person Adamski    schedule 06.07.2009
comment
Когда он создает новый char[], который удваивает предыдущий размер, выделяется ли вся память сразу?? Предположим, что предыдущий char[] равен 1 ГБ, он попытается немедленно выделить память для 2 ГБ? Или когда он на самом деле заполнен? - person erotsppa; 07.07.2009
comment
Он будет выделять новый массив только тогда, когда старый будет заполнен. - person Adamski; 07.07.2009
comment
таким образом, старый массив равен 1 ГБ, старый массив заполняется, создает новый массив 2 ГБ, копирует массив 1 ГБ в массив 2 ГБ (однако в настоящее время у вас есть 3 ГБ памяти) 1 ГБ теряет ссылку в ожидании сборки мусора, массив 2 ГБ становится новым хранилищем, и он остается пространство (1 ГБ, так как первый 1 ГБ был скопирован из старого массива) начинает использоваться. - person Sekhat; 07.07.2009
comment
Точно - Это настоящий убийца. - person Adamski; 07.07.2009
comment
Итак, ответ будет таким: использовать начальную емкость = файл.размер()? если возможно? - person OscarRyz; 07.07.2009
comment
Казалось бы, file.size() * 2, по крайней мере, плюс количество новых строк (для вставки дополнительных \r). - person Yishai; 07.07.2009
comment
Да, если вы заранее знаете размер (что, однако, должно было бы увеличить количество добавляемых символов), рекомендуется заранее выделить полный размер. - person Michael Borgwardt; 07.07.2009
comment
@Adamski, @Yishai: Почему file.size() * 2? Емкость StringBuffer считается в символах, а не в байтах, и вряд ли в файле может быть больше символов, чем байтов (при условии, что не используются экзотические кодировки). Начальная мощность file.size() + expectedLineCount * 2 была бы более экономичной. - person gustafc; 07.07.2009
comment
@Gustafc - Извинения; ты прав. Я удалю свой комментарий, чтобы не вызывать путаницы. - person Adamski; 07.07.2009
comment
@Adamski: обычно вам не следует отдавать предпочтение StringBuilder StringBuffer, потому что он быстрее. В частности, StringBuffer работает медленнее, поскольку является потокобезопасным. StringBuilder не является потокобезопасным. Если вы не имеете дело с несколькими потоками, вам следует использовать StringBuilder, поскольку он быстрее. - person Tom; 07.07.2009
comment
@Tom: Спасибо - я хотел написать быстрее, так как он не выполняет синхронизацию .. - person Adamski; 07.07.2009

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

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

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

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

person DaveR    schedule 06.07.2009
comment
Я уже подумал об этом, но до сих пор не объясняет, почему он преодолел 2 Гб (и, возможно, больше, не тестировал 2 Гб) - person erotsppa; 07.07.2009
comment
У вашего приложения НАМНОГО меньше доступной кучи, чем 2Gb. например в Windows адресное пространство одного процесса по умолчанию составляет всего 2 ГБ. В пределах этих 2 ГБ вы должны установить сопоставление для всех .dll, java vm, вероятно, зарезервирует некоторое пространство для себя и т. д. В оставшейся части у вас будет фрагментация памяти, предотвращающая перераспределение БОЛЬШОГО объекта, такого как ваш массив из быть перераспределенным (для чего нужно скопировать все, а затем освободить оригинал), потому что для такой большой вещи недостаточно места - только маленькие дыры свободного места, куда могут поместиться маленькие вещи. - person nos; 08.07.2009

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

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

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

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

person Michael Borgwardt    schedule 06.07.2009
comment
Как реализовать подкласс FilterInputStream, который добавляет разрывы строк на лету? - person erotsppa; 07.07.2009
comment
Просто расширьте FilterInputStream и перезапишите его методы read(), чтобы обнаруживать разрывы строк и возвращать \r\n, прежде чем продолжить работу с остальной частью базового потока. Это будет немного сложно, если вы хотите поддерживать отметку/сброс, но вам это, вероятно, не нужно. - person Michael Borgwardt; 07.07.2009
comment
Другой вопрос: чего вы на самом деле хотите добиться? Нормализовать разрывы строк? Кажется, это все, что на самом деле делает этот метод. - person Michael Borgwardt; 07.07.2009
comment
StringBuffer.toString() не всегда делает копию. Это копирование при записи, что означает, что копирование откладывается до следующего изменения StringBuffer. - person finnw; 07.07.2009
comment
Мои источники JDK 1.6.0u12 с вами не согласны. - person Michael Borgwardt; 07.07.2009
comment
Майкл Боргвардт: какой метод чтения перезаписывать? Много. Можете ли вы предоставить пример кода? - person erotsppa; 07.07.2009
comment
Вам придется перезаписать их все, но вы можете сделать так, чтобы основанные на массивах вызывали безпараметрическую, а последняя содержала всю вашу логику. - person Michael Borgwardt; 07.07.2009
comment
read() возвращает одно целое число, так как же мне вернуть \r\n? - person erotsppa; 07.07.2009
comment
Запоминая (в поле объекта), встречались ли вы только что с новой строкой, а затем возвращая эти символы в последовательных вызовах. - person Michael Borgwardt; 08.07.2009
comment
Хорошо, наконец, как мне реализовать логику на основе массива поверх безпараметрической? - person erotsppa; 08.07.2009
comment
Неважно, я скопировал из исходного кода Java. Не уверен, что это лучший способ сделать это. - person erotsppa; 08.07.2009

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

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

Изменить

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

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

person Andreas Dolk    schedule 06.07.2009
comment
-1 неправда; Размер StringBuffer удваивается, когда текущего размера недостаточно, а не небольшими приращениями. - person Michael Borgwardt; 07.07.2009
comment
@ Андреас, у меня с собой только JDK 1.5, но в общедоступном документе по Java говорится, что емкость увеличена как минимум вдвое, поэтому я не думаю, что они это меняют. Проверьте метод sureCapacity. Возможно, вы неправильно его читаете. - person Yishai; 07.07.2009
comment
Нет, разница заключается в длине абстрактной последовательности символов, которая, конечно же, увеличивается точно на количество добавленных символов, и размере базового массива, который может быть намного больше и расширяется большими шагами, чтобы уменьшить объем копирования. - person Michael Borgwardt; 07.07.2009

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

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

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

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

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

person Yishai    schedule 06.07.2009
comment
Вау, этот вопрос вызывает много отрицательных голосов за ответы. Но если будете минусовать, то хотя бы объясните почему. - person Yishai; 07.07.2009
comment
Использование конкатенации строк займет БЕЗУМНО много времени. Вполне возможно годы. Нет, я не преувеличиваю. - person Michael Borgwardt; 07.07.2009

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

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

person Peter Lawrey    schedule 07.07.2009

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

person David    schedule 07.07.2009

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

person joeslice    schedule 07.07.2009