Що таке витік пам'яті і як його виявити?
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!
Інструменти
| Інструмент | Коли | Overhead |
|---|---|---|
| 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 soft/weak/phantom → тільки сильні посилання
→ Показує, хто тримає об'єкт
Типові корені витоків:
- 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]]