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


Я разрабатываю приложение на Java, которое работает на устройствах Windows Mobile. Для достижения этой цели мы использовали Esmertec JBed JVM, который не является совершенным, но мы застряли с ним на данный момент. В последнее время мы получаем жалобы от клиентов по поводу OutOfMemoryErrors. После долгих игр с вещами я обнаружил, что устройство имеет много свободной памяти (прибл. 4 МБ).

OutOfMemoryErrors всегда возникают в одной и той же точке кода, и именно тогда расширение буфера строк для добавления к нему некоторых символов. После добавления некоторых журналов Вокруг этой области я обнаружил, что мой StringBuffer содержит около 290000 символов с емкостью около 290500. Стратегия расширения внутреннего массива символов заключается в том, чтобы просто удвоить его размер, поэтому он будет пытаться выделить массив примерно из 580000 символов. Я распечатал использование памяти примерно в это же время и обнаружил, что он использует около 3,8 МБ из общего объема около 6,8 МБ (хотя у меня есть видно, что общая доступная память время от времени увеличивается примерно до 12 МБ, так что есть много места для расширения). Таким образом, именно в этот момент приложение сообщает об ошибке OutOfMemoryError, что не имеет особого смысла, учитывая, сколько еще остается свободных.

Я начал думать о работе приложения до этого момента. В основном происходит то, что я разбираю XML-файл с помощью MinML (небольшой синтаксический анализатор XML Sax). Одно из полей в XML содержит около 300 тысяч символов. То парсер передает данные с диска и по умолчанию загружает только 256 символов одновременно. Таким образом, когда он достигает поля, о котором идет речь, синтаксический анализатор вызовет метод 'characters()' обработчика более 1000 раз. Каждый раз он будет создавать новый символ [], содержащий 256 символов. Обработчик просто добавляет эти символы в буфер строк. Начальный размер буфера строк по умолчанию равен всего 12, поэтому при добавлении символов в буфер он должен увеличиваться в несколько раз (каждый время создания нового символа []).

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

Итак, мой вопрос: есть ли у кого-нибудь опыт работы с этим СПМ и, возможно, сможет окончательно подтвердить или опровергнуть мои предположения о выделении памяти? А также, есть ли у кого-нибудь идеи (если мои предположения верны) о том, как улучшить распределение массивов, чтобы память не стала фрагментированной?

Примечание: вещи, которые я уже пробовал:

  • я увеличил начальный размер массива StringBuffer и увеличил размер чтения синтаксического анализатора, чтобы ему не нужно было создавать так много массивы.
  • Я изменил стратегию расширения Стрингбуффера таким образом, что как только он достигнет определенного порога размера, он будет расширяться только на 25%, а не на 100%.

Выполнение обоих этих действий немного помогло, но по мере увеличения размера xml-данных я все равно получаю OutOfMemoryErrors при довольно низком размере (ок. 350 КБ).

Еще одно дополнение: все эти испытания проводились на устройстве, использующем JVM, о котором идет речь. Если я запускаю тот же код на рабочем столе используя Java SE 1.2 JVM, у меня нет никаких проблем, или, по крайней мере, я не получаю проблему, пока мои данные не достигнут размера около 4 МБ.

Правка:

Еще одна вещь, которую я только что попробовал, которая немного помогла, - это то, что я установил Xms на 10 м. Таким образом, это проходит мимо проблемы JVM, не расширяя кучу, когда она должна, и позволяет мне обрабатывать больше данных, прежде чем произойдет ошибка.

6 13

6 ответов:

Может быть, вы могли бы попробовать VTD light. Он кажется более эффективным, чем саксофон. (Я знаю, что это огромная перемена.)

Просто ради обновления моего собственного вопроса я обнаружил, что лучшим решением было установить минимальный размер кучи (я установил его на 10 м). Это означает, что JVM никогда не должен решать, расширять или нет кучу, и поэтому он никогда (до сих пор в тесте) не умирает с OutOfMemoryError, даже если у него должно быть много места. До сих пор в тесте мы были в состоянии утроить объем данных, которые мы анализируем без ошибок, и мы, вероятно, могли бы пойти дальше, если бы нам действительно было нужно.

Это это немного хак для быстрого решения, чтобы сохранить существующих клиентов счастливыми, но теперь мы смотрим на другой JVM, и я сообщу об обновлении, если этот JVM справится с этим scneario лучше.

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

Чтобы подчеркнуть-вы получаете только ошибки "из памяти" после того, как GC был запущен, и все еще недостаточно памяти может быть освобожден.

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

На самом деле я не предлагаю, чтобы ваша виртуальная машина использовала простое копирование GC, я просто предлагаю исследовать это на уровне виртуальной машины.

Я думаю, что у вас достаточно памяти, но вы создаете огромное количество эталонных объектов. Попробуйте эту статью : https://web.archive.org/web/1/http://articles.techrepublic%2ecom%2ecom/5100-10878_11-1049545.html?tag=rbxccnbtr1 для получения дополнительной информации.

Я не уверен, что эти Стрингбуфферы выделяются внутри MinML - если да, то я предполагаю, что у вас есть источник для этого? Если вы это сделаете, то, возможно, при сканировании строки, если строка достигает определенной длины (скажем, 10000 байт), вы можете заглянуть вперед, чтобы определить точную длину строки и повторно выделить буфер до этого размера. Это некрасиво, но это сохранит память. (Это может быть даже быстрее, чем не делать lookaheads, так как вы потенциально экономите много переразмещения.)

Если у вас нет доступа к исходному тексту MinML, то я не уверен, каково время жизни буфера строк относительно XML-документа. Но это предложение (хотя оно еще более уродливо, чем предыдущее) все еще может сработать: поскольку вы получаете XML с диска, возможно, вы могли бы предварительно проанализировать его с помощью (скажем) Sax-синтаксического анализатора, исключительно чтобы получить размер строковых полей и соответственно распределить StingBuffers?

Можете ли вы получить дамп кучи с устройства?

Если вы получаете дамп кучи и он находится в совместимом формате, некоторые анализаторы памяти Java дают информацию о размере смежных блоков памяти. Я помню, что видел эту функциональность в IBM Heap Analyzer http://www.alphaworks.ibm.com/tech/heapanalyzer , но также проверьте более современный анализатор памяти Eclipse http://www.eclipse.org/mat/

Если у вас есть возможность изменить XML-файл, это был бы, вероятно, самый быстрый выход. Синтаксический анализ XML в Java всегда довольно интенсивен, и 300K-это довольно много для одного поля. Вместо этого вы можете попытаться разделить это поле на отдельный файл, не являющийся xml.