Устранение неполадок OutOfMemoryError в Java

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

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

1. Класс Java OutOfMemoryError

Класс OutOfMemoryError присутствует в пакете 'java.lang' и существует с версии Java 1.0. Он расширяет   Класс VirtualMachineError; следовательно, это непроверяемое исключение, и нам не нужно объявлять его в методе или конструкторе в условии throws.

Эта проблема скорее связана с ограничениями системы(объемом динамической памяти), а не с ошибками программирования, допущенными разработчиком.

Конструкторы, определенные в этом классе:

  • OutOfMemoryError(): создает экземпляр класса OutOfMemoryError, устанавливая null в качестве его сообщения.
  • OutOfMemoryError(String message): создает экземпляр класса OutOfMemoryError, используя указанное сообщение.

2. Различные причины и способы устранения ошибки OutOfMemoryError

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

Давайте теперь рассмотрим некоторые основные причины этой ошибки и соответствующие способы ее устранения:

2.1. Пространство кучи Java

Проблема

В Java все новые объекты хранятся в области кучи JVM, а память кучи имеет ограниченное пространство для хранения. Эта ошибка возникает, когда в памяти кучи недостаточно места для хранения новых объектов.

Это может произойти по разным причинам:

  • Когда мы пытаемся обработать большой объем данных или файлов, который слишком велик для хранения и обработки в памяти приложения.
  • Когда мы сохраняем ссылки на старые объекты(те, которые мы нигде не используем в нашем приложении), из-за этого куча памяти заполняется этими старыми объектами.
  • Когда у нас есть некоторые программные лазейки, и происходит утечка памяти в коде нашего приложения, которая без необходимости заполняет память кучи.

Давайте теперь рассмотрим пример создания проблемы пространства кучи Java:

public class JavaHeapSpace {public static void main(String[] args) throws Exception {String[] array = new String[100000 * 100000];}}

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

Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat memory.JavaHeapSpace.main(JavaHeapSpace.java:5)

Решение

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

Два основных атрибута JVM, которые определяют размер кучи Java, и мы можем задать их в настройках запуска приложения JVM:

  • -Xms для установки начального размера кучи
  • -Xmx для установки максимального размера кучи

Например, чтобы увеличить максимальный размер кучи до 8 ГБ, мы должны добавить параметр -Xmx8g к параметрам запуска JVM.

-Xms2G -Xmx8G

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

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

2.2. Лимит накладных расходов ГХ

Проблема

Эта ошибка возникает, когда сборщик мусора не может освободить память, несмотря на то, что он работает большую часть времени. Это означает, что JVM тратит примерно 98% своего времени на сборку мусора в течение 5 последовательных циклов сборки мусора и по-прежнему освобождает менее 2% пространства кучи.

Обычно эта ошибка возникает в старых версиях Java с меньшим объемом динамической памяти и старыми реализациями сборщиков мусора, таких как Parallel GC или Serial GC.

Давайте теперь рассмотрим пример создания предельной ошибки накладных расходов GC:

public class GCOverhead {public static void main(String[] args) throws Exception {Map<Long, Long> map = new HashMap<>();for(long i = 0l; i < Long.MAX_VALUE; i++) {map.put(i, i);}}}

Если мы запустим приведенный выше код с размером кучи 50 МБ и используем Java версии 8, которая использует Parallel GC, то мы получим следующую ошибку:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceededat java.base/java.lang.Long.valueOf(Long.java:1211)at memory.GCOverhead.main(GCOverhead.java:10)

Решение

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

-Xmx4G

2.3 Ограничения размера массива

Проблема

В приложении Java, если мы попытаемся создать массив размером больше Integer.MAX_INT или если наш массив станет слишком большим и его размер превысит размер кучи, то мы получим ошибку java.lang.OutOfMemoryError: Requested array size exceeds VM limit.

Например, если приложение пытается выделить массив размером 1024 МБ, но максимальный размер кучи составляет 512 МБ, то возникает эта ошибка.

Решение

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

-Xmx4G

2.4. Проблемы Пермского Ген.

Область памяти JVM разделена на 2 части:

  • Пространство кучи
  • PermGen(Постоянное поколение)

Во время запуска JVM настраивается размер пространства Heap и permgen. Пространство Heap хранит все вновь созданные объекты в Java, тогда как пространство PermGen отслеживает все загруженные метаданные классов, статические методы и ссылки на статические объекты.

Размер пространства PermGen в Java ограничен. В 64-разрядной версии JVM размер PermGen составляет около 85 МБ. Поэтому, если в приложении много классов, статических методов или ссылок на статические объекты, то это ограниченное пространство PermGen заполняется, и приложение выдает ошибку java.lang.OutOfMemoryError: PermGen space.

Начиная с версии Java 8, пространство PermGen удаляется из JVM, и поэтому мы больше не будем получать эту ошибку в версиях Java после JDK7.

Решение

Эту ошибку можно устранить, увеличив размер пространства PermGen, включив параметры JVM -XX:PermSize и -XX:MaxPermSize.

-XX:PermSize128m-XX:MaxPermSize512m

2.5. Проблемы метапространства

Поскольку пространство PermGen было удалено из JVM в рамках выпуска Java-8, все метаданные классов теперь хранятся в собственном пространстве, также называемом метапространством, которое является частью динамической памяти JVM.

Эта область метапространства по-прежнему ограничена и может быть исчерпана, если у нас много классов, что приведет к возникновению ошибки java.lang.OutOfMemoryError: Metaspace.

Решение

Мы можем устранить эту ошибку, увеличив размер Metaspace, добавив флаг -XX:MaxMetaspaceSize к аргументам VM или параметрам нашего приложения Java. Например, чтобы установить размер региона Metaspace равным 128M, мы добавим следующий параметр:

-XX:MaxMetaspaceSize128m

2.6. Вне обмена

Проблема

Проблемы на уровне операционной системы вызывают ошибку java.lang.OutOfMemoryError: Out of swap space, например:

  • Когда операционная система настроена с недостаточным объемом подкачки.
  • Когда другой процесс в системе потребляет все ресурсы памяти.

Решение

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

2.7 Количество потоков

Проблема

Существует ограничение на создание и запуск потоков в приложении, и ошибка java.lang.OutOfMemoryError: unable to create native thread возникает, когда мы достигаем порога создания потока и у операционной системы заканчиваются ресурсы для создания нового потока для нашего приложения.

Давайте теперь рассмотрим пример, чтобы более ясно понять эту ошибку.

public class ThreadsLimits {public static void main(String[] args) throws Exception {while(true) {new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000 * 60 * 60 * 24);} catch(Exception ex) {}}}).start();}}}

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

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reachedat java.base/java.lang.Thread.start0(Native Method)at java.base/java.lang.Thread.start(Thread.java:802)at memory.ThreadsLimits.main(ThreadsLimits.java:15

Решение

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

3. Как перехватить OutOfMemoryError

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

Чтобы отловить эту ошибку, нам просто нужно заключить код, который может вызвать проблемы с памятью, в блок try-catch,

public class OutOfMemoryErrorExample {public void initializeArray(int size) {try {Integer[] arr = new Integer[size];} catch(OutOfMemoryError e) {//Log the error msgSystem.err.println("Array size too large :: "+ e);}}public static void main(String[] args) {OutOfMemoryErrorExample obj = new OutOfMemoryErrorExample();obj.initializeArray(100000 * 100000);}}
Output:Array size too large :: java.lang.OutOfMemoryError: Java heap space

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

4. Различные инструменты для обнаружения ошибки OutOfMemoryError

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

Некоторые популярные инструменты для анализа OutOfMemoryError:

4.1.VisualVM

VisualVM — еще один инструмент командной строки JDK, который отслеживает и устраняет неполадки приложений, работающих на Java 1.4+. Он может отслеживать использование процессора приложением, активность GC, кучу и метапространство / постоянную генерацию памяти, количество загруженных классов и запущенных потоков.

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

4.2 Анализатор памяти Eclipse(MAT)

Eclipse Memory Analyzer — это базовый инструмент Eclipse, который используется для анализа дампа кучи Java. Он используется для поиска утечек памяти и утечек загрузчика классов в коде нашего приложения.

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

4.3.Visualgc

Visualgc(визуальный инструмент мониторинга сборки мусора) — еще один популярный инструмент, который можно подключить к инструментированной точке доступа JVM.

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

4.4.JProfiler

JProfiler — популярный инструмент профилирования Java на рынке. Он предоставляет интуитивно понятный пользовательский интерфейс для просмотра производительности системы, использования памяти, потенциальных утечек памяти и профилирования потоков.

Используя этот инструмент, мы можем легко отслеживать проблемы производительности и оптимизировать наше Java-приложение. Это кроссплатформенный инструмент, и его поддерживают различные операционные системы, такие как Windows, Mac OS, Linux, Solaris и т. д. Он также поддерживает различные IDE, такие как NetBeans, Eclipse, IntelliJ и т. д.

Этот инструмент предоставляет множество функций, таких как профилирование памяти, Heap Walker, профилирование ЦП, профилирование потоков, базы данных и т. д. для отслеживания проблем производительности в различных разделах. Он также обеспечивает поддержку как баз данных SQL, так и NoSQL.

5. Заключение

В этом уроке мы узнали о Java OutOfMemoryError и получили ответы на следующие вопросы:

  • Что такое класс OutOfMemoryError в Java?
  • Каковы различные причины и соответствующие исправления ошибки OutOfMemoryError?
  • Как отловить OutOfMemoryError в коде нашего приложения?
  • Различные инструменты, с помощью которых мы можем анализировать OutOfMemoryError, устранять утечки памяти и улучшать производительность приложения.
Прокрутить вверх