Избегайте фрагментации памяти при распределении множества массивов в Java

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

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

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

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

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

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

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

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

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

EDIT:

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

Ответы

Ответ 1

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

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

Ответ 2

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

Ответ 3

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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