Вопрос 21 · Раздел 3

Что такое memory leak и как его обнаружить?

4. Понять, почему они не удаляются

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Memory Leak (утечка памяти) — объекты, которые больше не нужны, но остаются в памяти.

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

Симптомы:

  • Приложение работает всё медленнее
  • GC работает постоянно
  • OutOfMemoryError в конце

Как обнаружить:

  1. Включить дампы: -XX:+HeapDumpOnOutOfMemoryError
  2. Открыть дамп в Eclipse MAT
  3. Найти самые большие объекты
  4. Понять, почему они не удаляются

🟡 Middle Level

Delta Analysis методология

1. Baseline: дамп после старта и прогрева
2. Нагрузка: стресс-тест
3. Cooldown: System.gc() для принудительной очистки (ТОЛЬКО в диагностике, НЕ в production!)
4. Snapshot: второй дамп
5. Сравнение: какие классы выросли?

Shallow vs Retained Heap

Shallow Heap = размер самого объекта
Retained Heap = всё, что удалится вместе с ним

Пример:
  Map node: 64 байта (Shallow)
  Держит: 500 МБ данных (Retained!)
  
→ В MAT смотрите Retained Heap!

Инструменты

Инструмент Когда Оверхед
Eclipse MAT Анализ дампов Offline (0%)
JFR/JMC Production мониторинг < 1%
jcmd Быстрая проверка Низкий
JProfiler Разработка Высокий

Типичные места утечек

// 1. Static collections
static Map<String, Object> cache = new HashMap<>();

// 2. ThreadLocal
ThreadLocal<User> user = new ThreadLocal<>();
// Забыли remove()

// 3. Listeners
eventBus.subscribe(listener);
// Забыли unsubscribe

// 4. Unclosed resources
InputStream is = new FileInputStream("file");
// Забыли close()

🔴 Senior Level

Dominator Tree

MAT: Dominator Tree
  → Показывает объекты, удерживающие больше всего памяти
  → Не обязательно самые большие сами по себе
  → Маленький объект может держать гигабайты!

Path to GC Roots

MAT: Path to GC Roots (exclude weak/soft)
  → Показывает цепочку ссылок до корня
  → Сразу видно, кто держит объект
  
Типичные корни утечек:
  - Static field → Map → ваш объект
  - ThreadLocal → ваш объект
  - Thread → ваш объект

OQL (Object Query Language)

-- Найти все строки > 1000 символов
SELECT s.value.toString() 
FROM java.lang.String s 
WHERE s.value.length > 1000

-- Найти дубликаты строк
SELECT toString(s.value) as val, count(*) as cnt
FROM java.lang.String s 
GROUP BY toString(s.value) 
HAVING count(*) > 100

JFR OldObjectSample

Java Flight Recorder:
  → Событие OldObjectSample
  → Показывает объекты, пережившие множество GC
  → Визуализация пути до GC Roots
  → БЕЗ снятия дампа!
  
→ Единственный безопасный способ для Heap > 100 ГБ

Sawtooth Pattern

Sawtooth Pattern (пила): «пол» (минимальное использование после GC) растёт от цикла к циклу.
Это означает: после каждой сборки остаётся всё больше объектов — утечка.

График памяти после GC:

Норма:
  |\    |\    |\
  | \   | \   | \
  |__\  |__\  |__\
  ← дно на одном уровне

Утечка:
  |\      |\        |\
  | \     | \       | \
  |__\    |___\     |____\
  ← дно растёт!

Production Experience

Реальный сценарий: ClassLoader Leak

  • Tomcat redeploy 10 раз → Metaspace 256 МБ → 4 ГБ
  • Причина: ThreadLocal хранил объект из ClassLoader приложения
  • Решение: ThreadLocal.remove() в ServletContextListener

Best Practices

  1. Delta Analysis — сравнивайте два дампа
  2. Retained Heap > Shallow Heap
  3. Path to GC Roots → exclude weak/soft
  4. JFR OldObjectSample для больших Heap
  5. Sawtooth Pattern → мониторинг
  6. HeapDumpOnOutOfMemoryError — обязательно
  7. Автоматизируйте снятие дампов

Резюме для Senior

  • Delta Analysis = сравнение дампов до/после
  • Retained Heap = реальное влияние памяти
  • Dominator Tree = поиск виновника
  • Path to GC Roots = цепочка ссылок
  • JFR OldObjectSample = дамп для больших Heap
  • Sawtooth Pattern = визуальный индикатор утечки
  • ClassLoader Leaks = самые коварные

🎯 Шпаргалка для интервью

Обязательно знать:

  • Memory Leak в Java — непреднамеренное удержание ссылок; GC не удаляет, потому что есть путь от GC Root
  • Delta Analysis: Baseline (дамп после прогрева) → Нагрузка → Cooldown → Snapshot → Сравнение растущих классов
  • Shallow Heap = размер объекта; Retained Heap = всё, что удалится вместе с ним → в MAT смотрите Retained!
  • Sawtooth Pattern: «пол» графика памяти растёт после каждой GC — визуальный индикатор утечки
  • Dominator Tree (MAT): показывает объекты, удерживающие больше всего памяти; маленький объект может держать гигабайты!
  • Path to GC Roots (exclude weak/soft): показывает цепочку ссылок до корня → 99% утечек = Static или Thread
  • JFR OldObjectSample: показывает объекты, пережившие множество GC; БЕЗ снятия дампа; для Heap > 100 ГБ

Частые уточняющие вопросы:

  • Почему Retained Heap важнее Shallow Heap? — Map node: 64 байта Shallow, но держит 500 МБ данных Retained; удалив node, освободите 500 МБ
  • Зачем Delta Analysis, а не один дамп? — Один дамп показывает «что есть»; два дампа показывают «что растёт» — утечка
  • Почему JFR лучше дампа для больших Heap? — Дамп 128 ГБ = STW 30-60 секунд; в Kubernetes Liveness Probe timeout → под убит; JFR < 1% overhead
  • Что такое OQL? — Object Query Language: SQL-подобные запросы к дампу (найти большие строки, дубликаты, утечки ThreadLocal)

Красные флаги (НЕ говорить):

  • «Делаю дамп в production на 128 ГБ без предупреждения» — STW 30-60 секунд, Liveness Probe fail → под убит
  • «System.gc() перед Delta Analysis — нормально» — НЕ в production! Только в диагностике, и то осторожно
  • «Shallow Heap показывает реальное влияние объекта» — Retained Heap показывает, сколько освободится при удалении

Связанные темы:

  • [[6. Что такое утечка памяти в Java]]
  • [[7. Как может возникнуть утечка памяти в Java]]
  • [[22. Какие инструменты помогают анализировать память]]
  • [[23. Что такое heap dump]]
  • [[25. Что такое GC roots]]