Питання 19 · Розділ 3

Що станеться при OutOfMemoryError?

4. Потік, що запросив пам'ять → помирає

Мовні версії: English Russian Ukrainian

🟢 Junior Level

OutOfMemoryError (OOME) — помилка, коли Java не може виділити пам’ять для об’єкта.

Що відбувається:

  1. Спроба виділити пам’ять → немає місця
  2. GC намагається очистити → все ще немає місця
  3. Видається OutOfMemoryError
  4. Потік, що запросив пам’ять → помирає

Види OOME:

  • Java heap space — Heap переповнений
  • Metaspace — метадані класів переповнені
  • GC overhead limit exceeded — GC працює постійно

🟡 Middle Level

Механіка OOME

1. Allocation Failure → немає пам'яті
2. Full GC → намагається очистити
3. SoftReference очищені → все ще немає
4. GC Overhead Limit — ЦЕ КОНКРЕТНИЙ тип OOME (GC overhead limit exceeded).
   Спрацьовує, коли JVM витрачає >98% часу на GC і звільняє <2% Heap.
   НЕ всі OOME проходять через цю перевірку — це окремий heuristic.
5. → OutOfMemoryError!

Zombie State

OOME може статися в БУДЬ-ЯКОМУ місці, включаючи виділення ресурсу.
Якщо OOME трапився при відкритті файлу → файл може залишитися відкритим.
Якщо OOME при захопленні lock → lock не буде звільнений.
⚠️ finally-блоки можуть НЕ виконатися, якщо OOME відбувається при виділенні пам'яті для самого фрейму стека.

OOME НЕ вбиває JVM миттєво!
  → Помирає тільки потік, що запросив пам'ять
  → Решта потоків працюють
  → АЛЕ: стан може бути corrupted!

JVM OOME vs OS OOM Killer

JVM OOME:
  → Бачите стек-трейс у логах
  → Є дамп хіпу

OS OOM Killer (Docker/K8s):
  → Процес просто зникає
  → У dmesg: "Out of memory: Kill process"
  → Немає логів Java!
  → Причина: RSS > ліміт контейнера

Production: Fail-Fast

# Обов'язково у production:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/dumps/
-XX:+ExitOnOutOfMemoryError

# → При OOME: дамп + вихід
# → Оркестратор перезапустить
# → Без zombie state

🔴 Senior Level

Thread Death і Corruption

OOME у потоці, що тримає Lock:
  → Lock не відпущений
  → Решта потоків чекають вічно
  → Deadlock!

OOME у ConcurrentHashMap:
  → Map у проміжному стані
  → Подальші операції → undefined behavior

Native Memory OOME

DirectByteBuffer витік:
  → Heap порожній
  → Native Memory = 100%
  → OOM Killer вбиває процес
  → Немає логів Java!

Діагностика:
  -XX:NativeMemoryTracking=detail
  dmesg | grep -i oom

Діагностика за типами

Тип помилки Причина Рішення
Heap space Витік або маленький -Xmx MAT аналіз дампа
GC overhead Хіп забитий впритул GC Logs, оптимізація
Metaspace Витік ClassLoader jcmd VM.metaspace
Direct buffer Витік Netty/NIO NMT
Native thread Ліміт потоків ОС ulimit -u

Heap Dump аналіз

# Автоматичний дамп при OOME
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps/

# Аналіз в Eclipse MAT:
# → Dominator Tree
# → Path to GC Roots
# → Знайти витік

Best Practices

  1. Ніколи не ловіть OOME у бізнес-коді
  2. ExitOnOutOfMemoryError у production
  3. HeapDumpOnOutOfMemoryError — обов’язково
  4. HeapDumpPath → зовнішній volume
  5. Моніторьте RSS і Native Memory
  6. NMT для off-heap витоків
  7. Fail-fast > zombie state

Резюме для Senior

  • OOME ≠ миттєва смерть JVM
  • Zombie state = corrupted locks і структури
  • OS OOM Killer = немає логів, процес зникає
  • Fail-fast = ExitOnOutOfMemoryError
  • Heap Dump = головний інструмент діагностики
  • Native Memory ≠ Heap (потрібен NMT)
  • Ніколи не catch OOME

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • OOME: JVM не може виділити пам’ять → GC намагається очистити → SoftReference очищені → все ще немає → OutOfMemoryError
  • OOME вбиває тільки потік, що запросив, НЕ всю JVM; решта потоків працюють у corrupted state (zombie state)
  • Zombie state: OOME при захопленні lock → lock не відпущений → deadlock; OOME у ConcurrentHashMap → undefined behavior
  • JVM OOME vs OS OOM Killer: JVM OOME дає стек-трейс і дамп; OS OOM Killer (Docker/K8s) — процес зникає, немає логів
  • GC Overhead Limit: окремий тип OOME; спрацьовує, коли JVM витрачає >98% часу на GC і звільняє <2% Heap
  • ExitOnOutOfMemoryError + HeapDumpOnOutOfMemoryError — обов’язково у production: дамп + вихід → оркестратор перезапустить
  • finally-блоки можуть НЕ виконатися, якщо OOME відбувається при виділенні пам’яті для самого фрейму стека

Часті уточнюючі запитання:

  • Чому не можна ловити OOME у бізнес-коді? — Стан corrupted: lock не відпущений, Map у проміжному стані; додаток ненадійний
  • Чим JVM OOME відрізняється від OS OOM Killer? — JVM OOME: бачите стек-трейс, є дамп; OS OOM Killer: dmesg | grep oom, процес убитий через SIGKILL, немає логів Java
  • Що таке zombie state? — JVM продовжує працювати після OOME одного потоку, але з corrupted locks/структурами → undefined behavior
  • Чому HeapDumpPath має бути на external volume? — У Kubernetes ефемерне сховище заб’ється дампом → под не перезапуститься

Червоні прапорці (НЕ говорити):

  • «Ловлю OOME і продовжую роботу» — zombie state, corrupted дані; краще fail-fast
  • «OOME миттєво вбиває JVM» — помирає тільки потік; JVM продовжує працювати у corrupted state
  • «OS OOM Killer — це проблема Java» — це рішення ОС; причина: RSS > ліміт контейнера, не -Xmx

Пов’язані теми:

  • [[20. Які типи OutOfMemoryError існують]]
  • [[21. Що таке витік пам’яті і як його виявити]]
  • [[23. Що таке heap dump]]
  • [[18. Що таке параметри -Xms та -Xmx]]
  • [[6. Що таке витік пам’яті в Java]]