Что такое memory leak и как его обнаружить?
4. Понять, почему они не удаляются
🟢 Junior Level
Memory Leak (утечка памяти) — объекты, которые больше не нужны, но остаются в памяти.
Простая аналогия: Вы выбросили мусор, но забыли закрыть дверь. Мусор продолжает копиться.
Симптомы:
- Приложение работает всё медленнее
- GC работает постоянно
OutOfMemoryErrorв конце
Как обнаружить:
- Включить дампы:
-XX:+HeapDumpOnOutOfMemoryError - Открыть дамп в Eclipse MAT
- Найти самые большие объекты
- Понять, почему они не удаляются
🟡 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
- Delta Analysis — сравнивайте два дампа
- Retained Heap > Shallow Heap
- Path to GC Roots → exclude weak/soft
- JFR OldObjectSample для больших Heap
- Sawtooth Pattern → мониторинг
- HeapDumpOnOutOfMemoryError — обязательно
- Автоматизируйте снятие дампов
Резюме для 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]]