Вопрос 7 · Раздел 3

Как может возникнуть утечка памяти в Java?

Утечка памяти возникает, когда ссылка на объект остаётся, хотя объект уже не нужен.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Утечка памяти возникает, когда ссылка на объект остаётся, хотя объект уже не нужен.

Самые частые причины:

1. Статические коллекции:

// ❌ Коллекция растёт бесконечно
static List<String> log = new ArrayList<>();
log.add("message");  // Никогда не очищается!

2. ThreadLocal без очистки:

// ❌ Забыли удалить
ThreadLocal<User> user = new ThreadLocal<>();
user.set(currentUser);
// В thread pool поток переиспользуется → старый User остаётся!

3. Не закрытые ресурсы:

// ❌ Забыли close()
FileInputStream fis = new FileInputStream("file.txt");
// Буферы остались в памяти

Как избежать:

  • Очищайте коллекции
  • Вызывайте ThreadLocal.remove()
  • Используйте try-with-resources

🟡 Middle Level

1. ThreadLocal в Thread Pools

// ❌ Проблема: потоки переиспользуются
public class UserContext {
    private static ThreadLocal<User> context = new ThreadLocal<>();
    
    public static void set(User user) {
        context.set(user);
    }
}

// Request 1: User A
UserContext.set(userA);
// ... обработка ...
// Забыли UserContext.remove()!

// Request 2 (тот же поток): User B
// context.get() → возвращает userA! ← Баг + утечка

Решение:

// ✅ Всегда удаляйте в finally
try {
    context.set(user);
    // обработка
} finally {
    context.remove();  // Обязательно!
}

2. Inner Classes (неявные ссылки)

// ❌ Нестатический внутренний класс
public class Outer {
    private byte[] data = new byte[1_000_000];  // 1 МБ
    
    class Inner {  // Неявно держит ссылку на Outer.this
        void doWork() { }
    }
    
    public void leak() {
        Inner inner = new Inner();
        executor.submit(inner::doWork);  // Inner передан в другой поток
        // Outer (1 МБ) не удалится, пока Inner жив!
    }
}

// ✅ Статический вложенный класс
static class Inner {  // Нет ссылки на Outer
    void doWork() { }
}

3. Static Collections

// ❌ Кэш без лимита
static Map<String, Data> cache = new HashMap<>();

public Data getData(String key) {
    if (!cache.containsKey(key)) {
        Data data = loadFromDb(key);
        cache.put(key, data);  // Растёт бесконечно!
    }
    return cache.get(key);
}

// ✅ Кэш с лимитом (Caffeine)
static Cache<String, Data> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();

4. String.intern() злоупотребление

// ❌ Интернирование миллионов уникальных строк
while (readingLogs) {
    String uniqueLog = readLine();
    uniqueLog.intern();  // В Java 7+ строки из StringTable могут быть удалены GC (если ClassLoader, загрузивший их, собран).
                         // Но при интенсивном intern() таблица растёт быстрее, чем очищается.
}

// StringTable в Heap → забивается → OOM

String.intern() полезен для дедупликации известного набора повторяющихся строк (enum-подобные значения, ключи). Вреден для уникальных строк (логи, UUID).

5. Listeners и Observers

// ❌ Подписались, но не отписались
eventBus.subscribe(listener);
// Объект listener не удалится, пока жив eventBus

🔴 Senior Level

ClassLoader Leak (Metaspace)

Динамическая генерация классов:
  Spring → прокси через CGLIB
  Hibernate → прокси для Entity
  Groovy/JavaScript → компиляция скриптов

Каждый сгенерированный класс → Metaspace
Классы очищаются только с ClassLoader

Если ClassLoader не GC:
  → Все классы остаются
  → Metaspace растёт → OOM

Причины утечки ClassLoader:
  1. ThreadLocal хранит объект из ClassLoader приложения
  2. DriverManager хранит ссылку на JDBC driver
  3. LogManager хранит ссылку на Logger приложения
  4. java.beans.Introspector кэширует BeanInfo

Диагностика:

# Проверка Metaspace
jcmd <pid> VM.metaspace

# Отслеживание загрузки классов
-Xlog:class+load=info
-XX:+TraceClassLoading
-XX:+TraceClassUnloading

# Если классов загружается больше, чем выгружается → утечка!

Off-Heap Memory Leaks

// DirectByteBuffer — память вне Heap
ByteBuffer buf = ByteBuffer.allocateDirect(100 * 1024 * 1024);
// 100 МБ вне -Xmx!

// Phantom Reference очищает буфер, когда ByteBuffer GC'd
// Но если GC не запускается (Heap пуст) → Native Memory растёт

// NIO Cleaner работает асинхронно
// → Задержка между "объект мёртв" и "память освобождена"

// Решение:
// 1. Ограничить: -XX:MaxDirectMemorySize=2g
// 2. Ручная очистка: ((DirectBuffer) buf).cleaner().clean();
//    ⚠️ Внимание: cleaner().clean() — внутренний JDK API, не гарантируется
//    между версиями. Используйте только как крайнюю меру.
// 3. Мониторинг: NMT

Dynamic Proxies и Metaspace

// Каждая генерация прокси → новый класс
for (int i = 0; i < 1_000_000; i++) {
    // Новый класс для каждого вызова!
    Object proxy = Proxy.newProxyInstance(
        classLoader,
        new Class<?>[] { Interface.class },
        handler
    );
}

// 1 млн классов → Metaspace OOM!
// Решение: кэшировать прокси

Production Experience

Реальный сценарий #1: Groovy скрипты

  • Приложение компилирует Groovy скрипты на лету
  • Каждый скрипт → новый ClassLoader → новые классы
  • За 2 недели: Metaspace 256 МБ → 4 ГБ → OOM
  • Решение: кэшировать скомпилированные скрипты

Реальный сценарий #2: Netty Direct Buffers

  • Netty выделяет DirectByteBuffer для каждого соединения
  • 10,000 соединений × 1 МБ = 10 ГБ Native Memory
  • Heap пустой (данные сразу отправляются) → GC не запускается
  • OOM Killer убивает процесс
  • Решение: -XX:MaxDirectMemorySize + pool буферов

Best Practices

  1. ThreadLocal.remove() в finally всегда
  2. Static collections → лимит + eviction policy
  3. Inner classes — делайте вложенный класс static, если ему НЕ нужен доступ к полям внешнего класса. Если доступ нужен — оставляйте нестатическим, но контролируйте время жизни.
  4. try-with-resources для всех Closeable
  5. Unsubscribe от listeners при уничтожении
  6. Кэшируйте прокси и скомпилированные скрипты
  7. MaxDirectMemorySize для контроля Native Memory
  8. NMT для мониторинга вне-heap памяти

Резюме для Senior

  • ThreadLocal — причина #1 в web-приложениях
  • Inner classes — неявная ссылка на outer
  • ClassLoader leaks — Metaspace растёт незаметно
  • DirectByteBuffer — утечка вне Heap (NMT нужен)
  • Dynamic proxies — кэшируйте!
  • String.intern() — осторожно с уникальными строками
  • Listeners — всегда отписывайтесь
  • Native Memory — может убить при пустом Heap

🎯 Шпаргалка для интервью

Обязательно знать:

  • ThreadLocal в thread pool: потоки переиспользуются → .remove() в finally обязателен
  • Нестатический inner class неявно держит ссылку на Outer.this → делайте static, если не нужен доступ к outer
  • Static коллекции без лимита — бесконечный рост → используйте Caffeine/Guava с maximumSize
  • String.intern() для уникальных строк (логи, UUID) забивает StringTable → OOM
  • ClassLoader Leak: динамическая генерация классов (Spring/CGLIB/Groovy) → Metaspace растёт
  • DirectByteBuffer — память вне Heap (-XX:MaxDirectMemorySize для контроля)
  • Listeners/Observers: подписались → отпишитесь при уничтожении

Частые уточняющие вопросы:

  • Почему Inner Class вызывает утечку? — Каждый нестатический inner class имеет неявное поле Outer.this; если inner передан в другой поток — весь outer не удалится
  • Как предотвратить ClassLoader Leak? — Кэшировать прокси и скомпилированные скрипты; проверить ThreadLocal, DriverManager, Introspector
  • Почему DirectByteBuffer опасен? — Память вне -Xmx; GC может не запускаться при пустом Heap → Native Memory растёт → OOM Killer убьёт процесс
  • Когда String.intern() полезен? — Для дедупликации известного набора повторяющихся строк (enum-подобные значения); вреден для уникальных

Красные флаги (НЕ говорить):

  • «ThreadLocal безопасен в Tomcat» — без .remove() это причина #1 утечек и security breach
  • cleaner().clean() для DirectByteBuffer — это внутренний JDK API, не гарантируется между версиями
  • «Inner class — это просто синтаксис, никакой утечки нет» — неявная ссылка на outer существует

Связанные темы:

  • [[6. Что такое утечка памяти в Java]]
  • [[11. Что такое Metaspace (или PermGen)]]
  • [[21. Что такое memory leak и как его обнаружить]]
  • [[2. Что хранится в Heap]]
  • [[18. Что такое параметры -Xms и -Xmx]]