Вопрос 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. Что такое memory leak и как его обнаружить]]
  • [[23. Что такое heap dump]]
  • [[18. Что такое параметры -Xms и -Xmx]]
  • [[6. Что такое утечка памяти в Java]]