Вопрос 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 → объект жив 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 убьёт процесс

Диагностика

Симптомы:

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