Хабрахабр

OutOfMemoryError: поймай, если сможешь

image

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

Предистория

Это достаточно тяжелая задача. У нас есть сервис, который по расписани закидывает задачу по обработке данных в ExecutorService. И в один прекрасный момент информации просто стало больше и она не влезла в наш -Xmx.

ООМ своими руками

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

public class MemoryGrabber }
}

Тут тоже присутствует некоторая проблема, но об этом позже.

Обычный код

public class BadExecutor { private static final Logger logger = LogManager.getLogger(BadThread.class); private static final ExecutorService executor = Executors.newFixedThreadPool(5); public static void main(String[] args) throws Exception { executor.submit(() -> { try { grabAllMemory(); } catch (Exception e) { logger.error(e.getMessage()); } }); }
}

Наверное, многие писали что-то подобное не один раз. Этот код вроде бы выглядит неплохо, ничего особенного тут нет. Ни в лог ни в поток вывода. Но проблема в том, что прм ООМ, не будет выведено вообще ничего.

Лови Throwable — говорили они

Поэтому тут он успешно прлетает мимо catch блока и перехватывается уже в коде ThreadPoolExecutor. Да точно, ведь OutOfMemoryError — это Error, а не Exception. Всем известно, что в самой корневой точке кода лучше ловить Throwable. Где проглатывается, а сам поток начинает ждать новой задачи.

При вызове logger.error(), мы просто получим новый ООМ, который так же канет в недрах ThreadPoolExecutor. К сожалению, если вместо Exception в данной ситуации поймать Throwable, ничего не изменится.

ThreadPoolExecutor же пытается переиспользовать потоки, что в принципе ожидаемо. Стоит заметить, что если бы вместо ExecutorService создавался бы новый Thread, то все ошибки в конечном счете были бы обработаны UncaughtExceptionHandler в случае смерти потока, и в stderr была бы информация.

Потерянный OutOfMemoryError

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

public class GetFuture { private static final Logger logger = LogManager.getLogger(BadThread.class); private static final ExecutorService executor = Executors.newFixedThreadPool(5); public static void main(String[] args) throws Exception { try { executor.submit(MemoryGrabber::grabAllMemory).get(); } catch (Throwable e) { logger.error(e); } }
}

Если logger.error() выкинет новый ООМ, то main поток свалится и, возможно, выведет ошибку. Теперь стало немного лучше. Все видели что-то подобное: Это помогает вытащить результат из ExecutorService наружу.

Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

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

UncaughtExceptionHandler — не панацея

лучше стало совсем немного. Не стоит радоваться раньше вреени, т.к. Если не переопределить обработчик, то вызывается ThreadGroup.uncaughtException(), в котором есть следующий код:

System.err.print("Exception in thread \"" + t.getName() + "\" ");
e.printStackTrace(System.err);

Тут все зависит от обстоятельств. Первая же строка создает новый объект при помощи конкатенации и, если там не вылетит новый ООМ, то есть большая вероятность получить его в printStackTrace(). Но суть в том, что даже получив ООМ в главном потоке, есть шанс ничего о нем не узнать.

Финализируй это

Из-за чего получаем вторую ошибку. Итак, теперь наша проблема в том, что нет памяти для логирования. Проблема заключчается в том, что MemoryGrabber.array — статическая переменная. Так может быть попробуем освободить пространство? Попробую ее почистить. И объекты доступные через нее GC считает живыми.

public class FinalizeIt { private static final Logger logger = LogManager.getLogger(BadThread.class); private static final ExecutorService executor = Executors.newFixedThreadPool(5); public static void main(String[] args) throws Exception { try { executor.submit(() -> { try { grabAllMemory(); } finally { MemoryGrabber.arrays.clear(); // Очищаем память } }).get(); } catch (Throwable e) { logger.error(e); } executor.shutdownNow(); }
}

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

Ода функциональному программированию

Она заключается в статической переменной array. Вначале я сказал, что в MemoryGrabber есть проблема. Очевыдным костылем является ее обнуление в блоке finaly. Дело в том, что эта переменная продолжает жить после того момента, как все свалилось с ошибкой. Было бы намного лучше, если она хранилась на стеке вызова.

public class FunctionalGrabber { public static void grabAllMemory() { List<Object[]> arrays = new LinkedList<>(); for (; ; ) { arrays.add(new Object[10]); } }
}

Не важно, с ошибкой или без. Теперь нашь лист List превратится в мусор как только завершится метод grabAllMemory. Почти Scala.

Как надо делать

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

  • -XX:+HeapDumpOnOutOfMemoryError и -XX:HeapDumpPath — сгенерируют дамп кучи во время ООМ, даже если приложение осталось работать
  • -XX:+ExitOnOutOfMemoryError и -XX:ExitOnOutOfMemoryErrorExitCode — позволяют просто завершить процесс с определенным кодом
  • -XX:+CrashOnOutOfMemoryError — остановит с ошибкой и создаст лог JVM

4. Последние два параметра появились только в JDK 8u92, остальные еще в 1. Такая логика — самая понятная для всех разработчиков и тех, кто будет поддерживать приложение. Оптимальным поведением является завершение процесса в случае OutOfMemoryError. Попытки обработать подобные ошибки могут привести к последствиям, неочевидным даже для самого автора.

Выводы

Чтобы их избежать, нужно иметь в виду: В статье я постарался разобраться в некоторых ошибках, из-за которых могут возникнуть проблемы при появлении ООМ.

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

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»