Как может возникнуть утечка памяти в Java?
Утечка памяти возникает, когда ссылка на объект остаётся, хотя объект уже не нужен.
🟢 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
- ThreadLocal.remove() в
finallyвсегда - Static collections → лимит + eviction policy
- Inner classes — делайте вложенный класс
static, если ему НЕ нужен доступ к полям внешнего класса. Если доступ нужен — оставляйте нестатическим, но контролируйте время жизни. - try-with-resources для всех Closeable
- Unsubscribe от listeners при уничтожении
- Кэшируйте прокси и скомпилированные скрипты
- MaxDirectMemorySize для контроля Native Memory
- 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]]