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

Що таке витік пам'яті і як його виявити?

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!

Інструменти

Інструмент Коли 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

  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]]