Що станеться при OutOfMemoryError
4. Смерть потоку: Потік, у якому сталася помилка, починає розгортання стека (stack unwinding). Якщо помилку не перехоплено, потік аварійно завершується. 5. Поведінка процесу: ⚠️...
🟢 Junior Level
java.lang.OutOfMemoryError (OOME) — це критична помилка віртуальної машини (Error, нащадок Throwable), яка виникає, коли JVM не може виділити пам’ять для нового об’єкта, а Garbage Collector не здатен звільнити достатній обсяг пам’яті.
Що відбувається при виникненні OOME:
- Спроба алокації: Потік намагається створити об’єкт через оператор
new, виділити масив чи буфер. - Запуск Garbage Collection: Пам’яті недостатньо $\rightarrow$ JVM ініціює екстрене збирання сміття (включно з очищенням
SoftReferenceта Full GC). - Генерація помилки: Якщо після всіх етапів очищення вільного неперервного блоку пам’яті потрібного розміру все ще немає, JVM кидає
OutOfMemoryError. - Смерть потоку: Потік, у якому сталася помилка, починає розгортання стека (stack unwinding). Якщо помилку не перехоплено, потік аварійно завершується.
- Поведінка процесу: ⚠️ Віртуальна машина JVM НЕ завершує роботу автоматично! Гине лише той потік, що запросив пам’ять. Решта потоків застосунку продовжують працювати, якщо не налаштовані спеціальні прапорці.
// Приклад коду, що викликає OOME
public class OomExample {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
// Рано чи пізно купа заповниться, і буде викинуто:
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
list.add(new byte[1024 * 1024 * 10]); // 10 МБ
}
}
}
🟡 Middle Level
1. Механіка виникнення OOME всередині JVM
Алокація об’єкта в JVM слідує суворому шляху ескалації:
[Потік: new Object()]
│
▼
[1. Спроба швидкої алокації в TLAB (Eden)] ───► Успіх ──► Завершено
│ (TLAB повний / об'єкт більший за TLAB)
▼
[2. Повільна алокація в Eden під локом] ─────► Успіх ──► Завершено
│ (Eden переповнений)
▼
[3. Minor / Young GC] ──────────────────────────► Успіх ──► Алокація
│ (Не допомогло / об'єкт занадто великий)
▼
[4. Очищення SoftReference + Full GC] ──────────► Успіх ──► Алокація
│ (Пам'яті все ще немає)
▼
[5. Перевірка GC Overhead Limit]
│ (Якщо >98% часу витрачається на GC, а звільняється <2% купи)
▼
[6. Викидання java.lang.OutOfMemoryError]
[!NOTE] Попередньо створений об’єкт OOME: Коли в купі взагалі немає вільного місця, JVM не може навіть створити новий об’єкт
OutOfMemoryErrorзі стек-трейсом. Тому HotSpot JVM використовує заздалегідь створений статичний екземпляр OOME, через що в логах іноді відображається повідомлення про помилку з порожнім стек-трейсом.
2. Стан зомбі (Zombie State) та псування даних
Оскільки за замовчуванням OOME вбиває лише один потік, застосунок переходить у вкрай небезпечний неконсистентний стан (Zombie / Corrupted State):
- Руйнування бізнес-транзакцій: Потік переривається на довільному рядку коду. Дані у спільних колекціях (
ConcurrentHashMap, чергах) можуть залишитися частково оновленими, порушуючи системні інваріанти. - Витік блокувань (Deadlock):
- Блокування моніторів (
synchronized) знімаються автоматично під час розгортання стека. - Однак явні блокування з
java.util.concurrent.locks(ReentrantLock,StampedLock), якщо OOME стався до входу в блокfinallyабо всередині критичної секції з порушенням інваріанта, можуть залишитися не знятими. Інші потоки зависнуть на них назавжди.
- Блокування моніторів (
- Придушення помилок пулами потоків: У серверах застосунків (Tomcat, Netty) або в
ThreadPoolExecutorпомилка OOME може бути перехоплена внутрішнім обробником пулу. Пул просто завершить робочий потік або створить новий, приховуючи катастрофу від логів верхнього рівня, доки всі потоки пулу по черзі не впадуть.
3. Fail-Fast стратегія в Production
Спроба відновити роботу застосунку після OOME всередині Java-коду безглузда й небезпечна. Єдино правильний патерн — Fail-Fast: миттєво зберегти діагностичний дамп пам’яті й перезапустити процес через оркестратор (Kubernetes / systemd).
# Обов'язкові production-прапорці JVM:
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/dumps/heap_dump.hprof \
-XX:+ExitOnOutOfMemoryError \
-jar app.jar
-XX:+HeapDumpOnOutOfMemoryError: зберігає повний знімок купи в момент першого викидання OOME.-XX:HeapDumpPath: шлях збереження дампу. Каталог має бути розташований на змонтованому постійному томі (Persistent Volume у K8s), інакше при перезапуску пода дамп буде знищено.-XX:+ExitOnOutOfMemoryError: негайно завершує JVM з ненульовим кодом при першому виникненні OOME, запобігаючи зависанню процесу в стані зомбі.-XX:+CrashOnOutOfMemoryError: альтернатива, що викликає аварійний збій JVM з генерацієюhs_err_pid.logта core dump (корисно для низькорівневого налагодження в C++ коді).
🔴 Senior Level
1. JVM OutOfMemoryError проти Linux OS OOM Killer
Найважливіша концептуальна різниця в production та мікросервісних архітектурах:
| Критерій | JVM OutOfMemoryError | Linux OOM Killer (cgroups) |
|---|---|---|
| Хто ініціює | Сама віртуальна машина HotSpot | Ядро операційної системи Linux |
| Причина | Перевищено ліміт Java Heap, Metaspace або GC Overhead | Процес перевищив ліміт фізичної RAM (RSS > cgroup memory limit) |
| Сигнал ОС | Немає (штатна обробка помилки в runtime) | Жорсткий сигнал SIGKILL (Signal 9, Exit Code 137) |
| Стек-трейс у Java логах | Є (java.lang.OutOfMemoryError: ...) |
Відсутній (процес моментально зникає) |
| Heap Dump | Знімається за наявності -XX:+HeapDumpOnOutOfMemoryError |
Не знімається, оскільки JVM не встигає зреагувати |
| Де шукати причину | Логи застосунку, Heap Dump, GC Logs | Системний журнал ядра: dmesg -T \| grep -i oom або journalctl |
# Діагностика OOM Killer у Linux/Kubernetes:
$ dmesg -T | grep -i "killed process"
# [Fri Sep 25 14:30:00 2026] Memory cgroup out of memory: Kill process 1234 (java) score 1024 or sacrifice child
# Killed process 1234 (java) total-vm:4582912kB, anon-rss:2097152kB, file-rss:0kB, shmem-rss:0kB
2. Чому не можна ловити catch (Throwable t) або catch (OutOfMemoryError e)?
// ❌ АНТИПАТЕРН: спроба обробки OOME в коді
try {
loadHugeDatasetIntoMemory();
} catch (OutOfMemoryError e) {
logger.error("Пам'ять закінчилася, очищаємо кеш і продовжуємо", e);
cache.clear();
}
Чому це катастрофа в production:
- Каскадне псування стану: Поки виконувався
loadHugeDatasetIntoMemory(), інші потоки паралельно виділяли об’єкти. Неможливо передбачити, які з цих потоків також отримали OOME і в якому стані залишилися внутрішні структури сервісу. - Вторинний OOME всередині catch/finally: При спробі залогувати помилку (
logger.error(...)) створюються нові об’єкти: рядки, масиви символів, події логера. Спроба виділити пам’ять усередині блоку обробки помилки викличе новий OOME, і потік миттєво впаде без запису в лог. - Блокування: Якщо метод перебував усередині синхронізованого блоку або працював зі структурами, що вимагають атомарного оновлення двох покажчиків, переривання залишає пам’ять у напівзруйнованому вигляді.
[!CAUTION] Єдине легітимне місце перехоплення OOME — це спеціалізовані системи профілювання, тестові фреймворки (що перевіряють поведінку під стресовим навантаженням) або моніторинговий
UncaughtExceptionHandler, завдання якого — відправити алерт у систему моніторингу та викликатиRuntime.getRuntime().halt(1).
3. Скрипти реагування: -XX:OnOutOfMemoryError
HotSpot підтримує можливість запуску зовнішньої команди операційної системи в момент настання OOME:
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/dumps/heap.hprof \
-XX:OnOutOfMemoryError="sh /scripts/notify_and_kill.sh %p" \
-jar app.jar
Де %p автоматично замінюється на PID процесу JVM, що впав. Скрипт може зібрати thread dump (jcmd %p Thread.print), зафіксувати мережеві з’єднання та примусово вбити зомбі-процес (kill -9 %p).
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь для співбесіди
«При виникненні
OutOfMemoryErrorJVM спочатку виконує повний цикл збирання сміття (Full GC) та очищенняSoftReference. Якщо пам’ять звільнити не вдалося, викидаєтьсяOutOfMemoryError. При цьому гине лише потік, що запросив пам’ять, а сама віртуальна машина продовжує працювати. Це переводить застосунок у небезпечний зомбі-стан із ризиком взаємних блокувань та спотворення даних. Тому в production обов’язковий підхід fail-fast: запуск із прапорцями-XX:+HeapDumpOnOutOfMemoryErrorта-XX:+ExitOnOutOfMemoryError, щоб негайно зберегти дамп для розслідування та дозволити оркестратору перезапустити под».
Питання підвищеної складності з відповідями
- Чи завершиться весь процес JVM, якщо у фоновому потоці виникне OutOfMemoryError?
- Відповідь: Ні, за замовчуванням віртуальна машина не завершує роботу. Гине лише той потік, у якому сталася помилка (розкручується його стек викликів). Якщо цей потік був частиною пулу потоків, пул може приховати падіння та продовжити приймати запити. Щоб процес гарантовано падав при OOME, необхідно використовувати прапорець
-XX:+ExitOnOutOfMemoryError.
- Відповідь: Ні, за замовчуванням віртуальна машина не завершує роботу. Гине лише той потік, у якому сталася помилка (розкручується його стек викликів). Якщо цей потік був частиною пулу потоків, пул може приховати падіння та продовжити приймати запити. Щоб процес гарантовано падав при OOME, необхідно використовувати прапорець
- У чому різниця між JVM OutOfMemoryError та збоєм через OOM Killer у Kubernetes (Exit Code 137)?
- Відповідь: При JVM OOME вичерпується внутрішній ресурс купи або метаспейсу JVM; при цьому віртуальна машина контролює процес, формує стек-трейс у логах і може зняти heap dump. При Linux OOM Killer процес перевищує ліміт фізичної пам’яті контейнера (RSS), і ядро ОС миттєво знищує JVM сигналом
SIGKILL. При OOM Killer процес зникає без логів Java і без можливості зняти heap dump, а запис про вбивство процесу залишається лише в системному журналі ядра ОС (dmesg).
- Відповідь: При JVM OOME вичерпується внутрішній ресурс купи або метаспейсу JVM; при цьому віртуальна машина контролює процес, формує стек-трейс у логах і може зняти heap dump. При Linux OOM Killer процес перевищує ліміт фізичної пам’яті контейнера (RSS), і ядро ОС миттєво знищує JVM сигналом
- Чому в логах
java.lang.OutOfMemoryErrorіноді не має стек-трейса?- Відповідь: Генерація стек-трейса вимагає виділення в пам’яті нових об’єктів (екземплярів
StackTraceElement). Якщо в момент настання OOME купа вичерпана повністю (до останнього байта), JVM не може виділити пам’ять під масив елементів стека і викидає заздалегідь створений глобальний статичний синглтонOutOfMemoryErrorбез трасування викликів.
- Відповідь: Генерація стек-трейса вимагає виділення в пам’яті нових об’єктів (екземплярів
- Чи виконається блок
finally, якщо всерединіtryстався OutOfMemoryError?- Відповідь: Блок
finallyспробує виконатися в процесі розкручування стека. Однак, якщо всередині самого блокуfinallyвідбувається будь-яка алокація об’єкта або якщо пам’яті недостатньо навіть для завантаження стекового фрейму методуfinally, JVM викине новий OOME, перервавши виконання блоку. Покладатися на виконанняfinallyпри OOME категорично не можна.
- Відповідь: Блок
Типові помилки та Red Flags
- 🚩 Стверджувати, що OOME одразу «роняє» всю віртуальну машину Java. (Red Flag: нерозуміння багатопотокової моделі Java та механіки обробки помилок).
- 🚩 Пропонувати огортати виклики в
catch (OutOfMemoryError e)для очищення колекцій. (Red Flag: нерозуміння стану неузгодженості пам’яті та ризику прихованих дедлоків). - 🚩 Плутати JVM Heap OOME та завершення процесу ядром Linux (Exit Code 137 / OOM Killer). (Red Flag: розробник годинами шукатиме витік у Heap Dump, коли реальна проблема полягала у витоку Native Memory, Netty Direct Buffers чи перевантаженні стеків потоків).