Главная » Хабрахабр » OutOfMemoryError: поймай, если сможешь

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 нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Учёные вырастили универсальные стволовые клетки при помощи CRISPR инженерии

Клетки сердечной мышцы человека, полученные из новых универсальных стволовых клеток Учёные из University of California San Francisco впервые вырастили универсальные стволовые клетки, используя технологию редактирования генов CRISPR в целях получения плюрипотентных стволовых клеток, которые могут быть трансплантированы любому пациенту, не ...

[Перевод] Шесть историй, как код переписали с нуля

Новый взгляд на извечный вопрос: следует ли переписывать приложение с нуля или это «самая худшая стратегическая ошибка, которую может сделать разработчик программного обеспечения»? Оказывается, при работе со зрелой кодовой базой есть более двух вариантов ответа. «Исходный код словно заржавел!» — Джоэл ...