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

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

ThreadLocal безпечний, якщо ви гарантовано викликаєте .remove() в finally і не передаєте посилання на об'єкт назовні з потоку.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Витік пам’яті (Memory Leak) — коли об’єкти, які більше не потрібні, залишаються в пам’яті і не видаляються GC.

У C/C++ витік — це коли пам’ять звільнена, але вказівник залишився (dangling pointer). У Java — навпаки: об’єкт міг би бути видалений, але посилання його утримує.

Проста аналогія: Ви викинули сміття, але забули закрити кришку. Сміття накопичується, хоча ви його “викинули”.

У Java витік — це не “втрата” пам’яті, а її **утримання:**

  • Об’єкт більше не потрібен → але посилання на нього залишилось
  • GC не може видалити → є посилання
  • Пам’ять поступово закінчується → OutOfMemoryError

Приклад:

// ❌ Витік: об'єкти додаються, але не видаляються
static List<String> history = new ArrayList<>();

public void add(String data) {
    history.add(data);  // Росте нескінченно!
    // Старі дані вже не потрібні, але залишаються в пам'яті
}

Симптоми:

  • Може проявлятися як сповільнення (через частий GC), але часто симптомів немає до раптового OOM.
  • GC працює постійно
  • Зрештою: OutOfMemoryError

Коли ThreadLocal безпечний

ThreadLocal безпечний, якщо ви гарантовано викликаєте .remove() в finally і не передаєте посилання на об’єкт назовні з потоку.

🟡 Middle Level

Механізм витоку

Витік у Java = ненавмисне утримання посилань

Об'єкт живий, поки є шлях від GC Roots:
  GC Root → Static Field → Map → Your Object

Якщо забули прибрати з Map → об'єкт живий назавжди!

Типи витоків

1. On-Heap Leaks (у Heap):

// Статичні колекції
static Map<String, Object> cache = new HashMap<>();
cache.put(key, value);  // Ніколи не видаляється

// ThreadLocal
ThreadLocal<User> user = new ThreadLocal<>();
user.set(currentUser);
// Забули user.remove() → об'єкт живий, поки живий потік

// Event Listeners
button.addActionListener(listener);
// Забули removeListener → listener живий назавжди

2. Metaspace Leaks:

// Витік ClassLoader-ів
// При redeploy старий ClassLoader не вивантажується
 Всі класи залишаються в Metaspace
 OutOfMemoryError: Metaspace

3. Off-Heap Leaks (Native Memory):

// DirectByteBuffer — пам'ять поза Heap
ByteBuffer buf = ByteBuffer.allocateDirect(100 * 1024 * 1024);
// 100 МБ поза -Xmx!
// Якщо не очищується → OOM Killer уб'є процес

Діагностика

Симптоми:

  1. Sawtooth Pattern — графік пам’яті росте після кожного збору
  2. GC Thrashing — GC працює постійно, CPU 100%
  3. OutOfMemoryError — кінцевий результат

Інструменти:

# Зняти дамп пам'яті при OOM
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/dump.hprof

# Аналіз через Eclipse MAT
# → Dominator Tree → Path to GC Roots
# → Знайти, хто тримає об'єкти

Типові джерела

// 1. Static collections
static List<Data> allData = new ArrayList<>();

// 2. ThreadLocal без cleanup
ThreadLocal<Connection> conn = new ThreadLocal<>();
// У thread pool потоки перевикористовуються → дані накопичуються

// 3. Inner classes (неявне посилання на outer class)
class Outer {
    class Inner { }  // Тримає посилання на Outer.this
}

// 4. Unclosed resources
InputStream is = new FileInputStream("file.txt");
// Забули is.close() → буфери в пам'яті

🔴 Senior Level

Анатомія витоку: Shallow vs Retained Heap

Shallow Heap = розмір самого об'єкта
  Object Header (16) + поля + padding

Retained Heap = все, що видалиться разом з об'єктом
  Об'єкт + всі об'єкти, доступні ТІЛЬКИ через нього

Приклад:
  Map node: 64 байти (Shallow)
  Але тримає: 500 МБ даних (Retained!)

→ В MAT дивіться Retained Heap, не Shallow!

ClassLoader Leak Deep Dive

ClassLoader → завантажив 1000 класів
Кожен клас → InstanceKlass в Metaspace
Кожен клас → static поля → об'єкти в Heap

Якщо ClassLoader не GC:
  → 1000 класів залишаються в Metaspace
  → Всі static поля залишаються в Heap
  → Всі об'єкти, на які вони посилаються → теж

Причини:
  - ThreadLocal зберігає клас з ClassLoader
  - Static поле в системному класі посилається на додаток
  - JDBC driver зареєстрований у DriverManager
  - Loggers тримають посилання на класи додатку

Native Memory Tracking (NMT)

# Увімкнути NMT
-XX:NativeMemoryTracking=detail

# Аналіз
jcmd <pid> VM.native_memory summary

# Вивід:
Native Memory Tracking:
  Java Heap: 2 GB
  Class: 50 MB
  Thread: 100 MB
  Code: 80 MB
  GC: 200 MB
  Compiler: 30 MB
  Internal: 50 MB
  Symbol: 20 MB
  Native Memory Tracking: 10 MB
  Arena Chunk: 5 MB
  Unknown: 15 MB  ← Витік тут?

Distributed Systems та витоки

У кластері витік може маскуватися:
  Node 1: пам'ять росте → балансировщик перемикає на Node 2
  Node 2: пам'ять росте → перемикає на Node 3
  → Лавиноподібне падіння всього кластера!

Рішення:
  - Liveness/Readiness проби
  - Fail-fast при OOME
  - Моніторинг пам'яті на кожному вузлі

Production Experience

Реальний сценарій #1: ThreadLocal у Tomcat

  • Web додаток: ThreadLocal для зберігання UserContext
  • Забули remove() у finally
  • Tomcat thread pool: потоки перевикористовуються
  • Результат: UserContext користувача A доступний користувачу B
    • Memory Leak: Context накопичувався 3 місяці → Metaspace OOM

Це ДВІ проблеми: (1) витік пам’яті — Context накопичується і не видаляється, (2) security breach — дані одного користувача доступні іншому через неочищений ThreadLocal.

Реальний сценарій #2: DirectByteBuffer

  • Netty сервер: allocateDirect() для кожного запиту
  • GC не запускається (Heap майже порожній)
  • Native Memory: 8 ГБ → OOM Killer убив процес
  • Рішення: -XX:MaxDirectMemorySize=2g + моніторинг

Best Practices

  1. Завжди закривайте ресурси (try-with-resources)
  2. ThreadLocal.remove() у finally
  3. Inner classes → static якщо не потрібен outer
  4. Кеши з лімітом (Caffeine, Guava)
  5. HeapDumpOnOutOfMemoryError — обов’язково у production
  6. NMT для моніторингу Native Memory
  7. Delta Analysis — порівнюйте дампи до/після навантаження
  8. Fail-fast — краще померти, ніж працювати з corrupted state

Резюме для Senior

  • Витік у Java = unintended reference retention
  • Shallow vs Retained Heap — дивіться Retained!
  • ClassLoader Leaks — найбільш підступні (Metaspace)
  • Native Memory — не видна у Heap Dump (потрібен NMT)
  • ThreadLocal — головний підозрюваний #1
  • Delta Analysis — порівнюйте два дампи
  • Distributed — витік може вбити весь кластер
  • Fail-fast > zombie state

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Витік у Java — це не втрата пам’яті, а ненавмисне утримання посилань (GC не може видалити, бо є шлях від GC Root)
  • Shallow Heap = розмір самого об’єкта; Retained Heap = все, що видалиться разом з об’єктом — у MAT дивіться Retained!
  • Top-3 джерела витоків: статичні колекції, ThreadLocal без .remove(), не закриті ресурси
  • 3 типи витоків: On-Heap (у Heap), Metaspace (ClassLoader leaks), Off-Heap (DirectByteBuffer)
  • HeapDumpOnOutOfMemoryError — обов’язково у production
  • Sawtooth Pattern: «підлога» графіку пам’яті росте після кожного GC — ознака витоку
  • У distributed-системі витік на одному вузлі може спричинити лавиноподібне падіння кластера

Часті уточнюючі запитання:

  • Чим витік у Java відрізняється від витоку в C? — У C пам’ять звільнена, але вказівник залишився (dangling pointer); у Java — навпаки: об’єкт міг би бути видалений, але посилання утримує
  • Чому ThreadLocal небезпечний у thread pool? — Потоки перевикористовуються; дані попереднього запиту залишаються і доступні наступному
  • Що таке Delta Analysis? — Порівняння двох heap dump (до/після навантаження) для виявлення класів, що ростуть
  • Чому Inner Class може викликати витік? — Нестатичний внутрішній клас неявно тримає посилання на Outer.this

Червоні прапорці (НЕ говорити):

  • «У Java не буває витоків пам’яті, є GC» — витоки бувають, через утримання посилань
  • «ThreadLocal безпечний, бо потік помре» — У thread pool потоки НЕ вмирають
  • «Достатньо збільшити -Xmx і витік зникне» — це маскує проблему, OOM все одно станеться

Пов’язані теми:

  • [[5. Коли об’єкт стає кандидатом на видалення GC]]
  • [[7. Як може статися витік пам’яті в Java]]
  • [[21. Що таке memory leak і як його виявити]]
  • [[23. Що таке heap dump]]
  • [[11. Що таке Metaspace (або PermGen)]]