Що таке витік пам'яті в Java?
ThreadLocal безпечний, якщо ви гарантовано викликаєте .remove() в finally і не передаєте посилання на об'єкт назовні з потоку.
🟢 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 уб'є процес
Діагностика
Симптоми:
- Sawtooth Pattern — графік пам’яті росте після кожного збору
- GC Thrashing — GC працює постійно, CPU 100%
- 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
- Завжди закривайте ресурси (try-with-resources)
- ThreadLocal.remove() у
finally - Inner classes → static якщо не потрібен outer
- Кеши з лімітом (Caffeine, Guava)
- HeapDumpOnOutOfMemoryError — обов’язково у production
- NMT для моніторингу Native Memory
- Delta Analysis — порівнюйте дампи до/після навантаження
- 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)]]