Что такое утечка памяти в 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 → объект жив forever!
Типы утечек
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 жив forever
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)]]