Was java lang outofmemoryerror

Когда заканчивается оперативная память (OutOfMemoryError)

Тема данного поста — довольно часто встречающаяся проблема переполнения памяти в Java. Постараюсь рассказать, почему этой ошибке стоит уделить особое внимание, почему так сложно искать причину и какие утилиты в этом помогают.

Ошибка OutOfMemoryError в java неизбежна. Если в нагруженном приложении такая ошибка не возникает, значит она была раньше и её починили. Я немного утрирую, но я не помню проекта, на котором бы такая ошибка рано или поздно не проявлялась. На собеседованиях вопрос про OutOfMemory не очень часто задают; честно говоря, редко кто отвечает правильно и подробно.

Даже несмотря на то, что такая ошибка может приводить к реальным убыткам, так как чаще всего возникает под нагрузкой, на production instance (продакшне, по-народному) и приводит к параличу системы на часы и более, всё равно в 90% случаев вместо поиска причины ошибки люди увеличивают объём памяти JVM (хоть до -Xmx64g) и настраивают еженедельные, а потом и ежедневные рестарты сервера. Чтобы система не повисала, настраивают мониторинги и когда память приближается к пороговому значению, проводят небольшой анализ, который, как правило, ничего не даёт, а уже потом перезагружают сервер.

Но что делать если рестарт или увеличение памяти не помогают? Тогда вся команда бросит свои текущие задачи и будет искать причину, воспроизводить, изучать логи, дампы памяти и потоков. Тогда, конечно, причину найдут и починят. Конечно, не всегда всё так серьезно, но бывает и так.

картинки нет, но вы держитесь

Как вообще происходит управление памятью в Java? Тот, кто знаком с языками С или C++, знает каково это явно выделять память под каждый массив или объект и потом так же явно освобождать её. Пример работы с массивом в C++: В документации сказано, что значение в секундах, но Tomcat в текущей реализации умножает на 60, поэтому 1 — одна минута. Время в секундах можно выставить прямо на объекте сессии методом setMaxInactiveInterval. Перезапускаем тест, память больше не переполняется. Мне ещё потребовалось увеличить память до 150мб (-Xmx150m), потому что иначе ошибка выпадала быстрее, чем за минуту. Наглядно:

картинки нет, но вы держитесь

Сессия не создавалась бы вовсе, если бы не вот эта строчка кода, которая имитирует неаккуратное логирование, приводящее к побочным эффектам:

 log.trace("req from <>, session id <>", request.getRemoteHost(), request.getSession().getId()); 

Если её удалить или использовать дополнительный параметр при вызове getSession (request.getSession(false)), то сессия не будет создаваться вообще. Такое поведение — скорее исключение, потому что как минимум аутентификации хранится в сессии, а уж аутентификация есть практически в любом корпоративном приложении. Чтобы сервисы не имели состояния (stateless), нужно заранее проектировать систему таким образом, использовать явные настройки и тестировать их. Например, в Spring Security есть разные конфигурации создания сессии: always, ifRequired, never, stateless. Это отдельная тема. Здесь же я хотел показать общую схему поиска ошибки:

  1. Воспроизведение OutOfMemoryError
  2. Получение дампа памяти
  3. Диагностика приложения в динамике
  4. Анализ дампа памяти, гипотеза
  5. Проверка гипотезы
  6. Hotfix: Быстрое решение, чтобы починить приложение прямо сейчас
  7. Long term solution: Правильное решение, исправление кода или даже изменение дизайна.
Читайте также:  Ошибка при загрузке python

У нас быстрое решение — уменьшить таймаут сессии и, по возможности дать больше памяти процессу. Правильное решение — перейти на stateless сервис, удалить ошибочное создание сессии без необходимости либо уменьшить размер данных, хранящихся в сессии, перейдя на специализированные библиотеки кэширования.

Если Вы сами повторяли шаги анализа, то могли заметить, насколько приложение замедляется перед тем, как случается ошибка OutOfMemoryError. Плата за удобство управления памятью — время, которое требуется сборщику мусора на каждый цикл его работы. Реализация сборщика мусора G1, которая используется по умолчанию, периодически полностью останавливает выполнение команд. Это называется Stop-the-world. Другие стандартные реализации поступают так же: Serial, Parallel, CMS. В обычном состоянии эти паузы малы и практически не влияют на производительность, хотя и не позволяют использовать Java для «real time» приложений. Но когда существует утечка и свободной памяти остаётся всё меньше, работа сборщика мусора превращается в сизифов труд. Такое состояние ещё хуже, чем если бы ошибка выпадала сразу. Иногда JVM удаётся определить подобное состояние и выбрасывается исключение:

 java.lang.OutOfMemoryError: GC overhead limit exceeded

Чаще всего это означает утечку памяти, заполняющую её не очень быстро. Отдельной темой будет анализ логов сборщика мусора, поиск там событий «Full GC» и так далее. Приведу лишь ссылку на статью на эту тему. По моему опыту, эффективный анализ производится по дампам памяти, а логи GC используются, чтобы исключить из рассмотрения проблемы утечек памяти при анализе проблем с производительностью приложения. Как правило, при достижении верхней границы памяти события Full GC начинают происходить одно за другим.

В заключении приведу несколько примеров реальных и выдуманных ситуаций, приводящих к переполнению памяти и варианты решения.

Кэширование

Если применяется библиотека кэширования, то следует ограничивать не только максимальное время жизни объектов и их количество, но и максимальный объём памяти. Тогда переполнения точно не случится, в худшем случае мы получим слишком быстрое удаление объектов из кэша. Пример конфигурации для Hazelcast:

  id="PartitionRegionTemplate" template="ExtendedRegionTemplate" copies="1" load-factor="0.70" local-max-memory="1024" total-max-memory="16384" value-constraint="java.lang.Object"> 

С другой стороны, библиотека JCS, например, не позволяет явно указывать максимальный объём памяти (по крайней мере, я в своё время не нашёл такой конфигурации). В общем, идея в том, что ограничивать количество объектов имеет смысл, если их размер стабилен, но и тогда придётся считать и исследовать. А ограничив общее потребление памяти, легко подобрать конфигурацию так, чтобы суммарно весь кэш занимал предсказуемый объём памяти и не приводил к переполнению.

Неожиданно большие коллекции в базе данных

При использовании в JPA отношений OneToMany или ManyToMany, когда загрузка одного объекта из базы влечет за собой загрузку списка связанных объектов, существует два варианта загрузки: ленивый и жадный (Lazy, Eager). При разумном подходе все коллекции либо, по крайней мере, большую часть помечают как Lazy. Это означает, что они не будут инициализироваться без необходимости. Но для удобства часть коллекций можно оставить как Eager. Подобные коллекции могут привести к переполнению памяти, если их размер со временем разрастется в связи со спецификой нагрузки на продакшне. Другой случай — коллекция действительно нужна в конкретном сценарии использования. Скажем, мы показываем администратору неудачные попытки ввода пароля за сутки на одной странице. Обычно их 10 — 100. Код может работать отлично, пока это предположение не нарушено, но в один прекрасный день в результате какой-нибудь DDOS атаки или просто ошибки в коде, коллекция вырастает до 500 000 элементов, и приложение закономерно падает с OutOfMemoryError. На практике лучше избегать предположений по размеру коллекций. Возможно, для этого придётся поменять бизнес требования, реализовывать постраничное отображение информации или ограничения (constraints) на уровне базы данных.

Неожиданно большие файлы

Если входные данные от сторонних систем поступают в виде файлов, нужно избегать полной загрузки файла в память. Скорее даже не самого файла, а объектной модели, соответствующей его содержимому. Для большинства форматов данных (например, JSON, XML, CSV) существуют способы потоковой обработки информации без отказа от удобной объектной модели.

Утечки в сторонних библиотеках

Они — возможны, но намного реже утечек в коде приложения. Такие проблемы исследовать очень тяжело. Если на основе анализа в Eclipse MAT появляется гипотеза о некорректном поведении библиотеки, то чтобы её проверить может потребоваться разработка синтетического приложения, которое отражает только один сценарий работы приложения, связанный с этой библиотекой, а всё остальное из него исключено. Впрочем, этот подход справедлив для исследования любых подозрений на ошибки в сторонних библиотеках, не только об утечках памяти.

Заключение

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

Источник

Какие бывают типы OutOfMemoryError или из каких частей состоит память java процесса

Если вы словили OutOfMemoryError, то это вовсе не значит, что ваше приложение создает много объектов, которые не могут почиститься сборщиком мусора и заполняют всю память, выделенную вами с помощью параметра -Xmx. Я, как минимум, могу придумать два других случая, когда вы можете увидеть эту ошибку. Дело в том, что память java процесса не ограничивается областью -Xmx, где ваше приложение программно создает объекты.

image

Область памяти, занимаемая java процессом, состоит из нескольких частей. Тип OutOfMemoryError зависит от того, в какой из них не хватило места.

1. java.lang.OutOfMemoryError: Java heap space

Не хватает место в куче, а именно, в области памяти в которую помещаются объекты, создаваемые программно в вашем приложении. Размер задается параметрами -Xms и -Xmx. Если вы пытаетесь создать объект, а места в куче не осталось, то получаете эту ошибку. Обычно проблема кроется в утечке памяти, коих бывает великое множество, и интернет просто пестрит статьями на эту тему.

2. java.lang.OutOfMemoryError: PermGen space

Данная ошибка возникает при нехватке места в Permanent области, размер которой задается параметрами -XX:PermSize и -XX:MaxPermSize. Что там лежит и как бороться с OutOfMemoryError возникающей там, я уже описал подробнейшим образом тут.

3. java.lang.OutOfMemoryError: GC overhead limit exceeded

Данная ошибка может возникнуть как при переполнении первой, так и второй областей. Связана она с тем, что памяти осталось мало и GC постоянно работает, пытаясь высвободить немного места. Данную ошибку можно отключить с помощью параметра -XX:-UseGCOverheadLimit, но, конечно же, её надо не отключать, а либо решать проблему утечки памяти, либо выделять больше объема, либо менять настройки GC.

4. java.lang.OutOfMemoryError: unable to create new native thread

Впервые я столкнулся с данной ошибкой несколько лет назад, когда занимался нагрузочным тестированием и пытался выяснить максимальное количество пользователей, которые могут работать с нашим веб-приложением. Я использовал специальную тулзу, которая позволяла логинить пользователей и эмулировать их стандартные действия. На определенном количестве клиентов, я начал получать OutOfMemoryError. Не особо вчитываясь в текст сообщения и думая, что мне не хватает памяти на создание сессии пользователя и других необходимых объектов, я увеличил размер кучи приложения (-Xmx). Каково же было мое удивление, когда после этого количество пользователей одновременно работающих с системой только уменьшилось. Давайте подробно разберемся как же такое получилось.

На самом деле это очень просто воспроизвести на windows на 32-битной машине, так как там процессу выделяется не больше 2Гб.

Допустим у вас есть приложение с большим количеством одновременно работающих пользователей, которое запускается с параметрами -Xmx1024M -XX:MaxPermSize=256M -Xss512K. Если всего процессу доступно 2G, то остается свободным еще коло 768M. Именно в данном остатке памяти и создаются стеки потоков. Таким образом, примерно вы можете создать не больше 768*(1024/512)=1536 (у меня при таких параметрах получилось создать 1316) нитей (см. рисунок в начале статьи), после чего вы получите OutOfMemoryError. Если вы увеличиваете -Xmx, то количество потоков, которые вы можете создать соответственно уменьшается. Вариант с уменьшением -Xss, для возможности создания большего количества потоков, не всегда выход, так как, возможно, у вас существуют в системе потоки требующие довольно больших стеков. Например, поток инициализации или какие-нибудь фоновые задачи. Но все же выход есть. Оказывается при программном создании потока, можно указать размер стека: Thread(ThreadGroup group, Runnable target, String name,long stackSize). Таким образом вы можете выставить -Xss довольно маленьким, а действия требующие больших стеков, выполнять в отдельных потоках, созданных с помощью упомянутого выше конструктора.

Более подробно, что же лежит в стеке потока, и куда уходит эта память, можно прочитать тут.

Конечно, вам может показаться данная проблема слегка надуманной, так как большинство серверов нынче крутиться на 64-битной архитектуре, но все же считаю данный пример весьма полезным, так как он помогает разобраться из каких частей состоит память java-процесса.

Источник

Оцените статью