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

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

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

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

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

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

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

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

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

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

EDIT:

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

Ответы [ 6 ]

2 голосов
/ 18 января 2010

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

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

2 голосов
/ 15 января 2010

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

1 голос
/ 15 января 2010

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

Подчеркнем - вы получаете только ошибки "нехватки памяти" после ГХ был запущен, но все еще недостаточно памяти.

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

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

0 голосов
/ 14 января 2010

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

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

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

0 голосов
/ 14 января 2010

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

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

0 голосов
/ 14 января 2010

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

...