← Назад к вопросам

Почему возникает утечка памяти?

1.7 Middle🔥 131 комментариев
#JVM и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Утечки памяти в Java

Утечка памяти (Memory Leak) в Java — это ситуация, когда объекты остаются в памяти, хотя приложение их больше не использует. Это приводит к постоянному увеличению потребления памяти и в итоге может привести к OutOfMemoryError.

Важно понимать: в Java есть автоматическая сборка мусора (Garbage Collector), но она не может удалить объекты, если на них всё ещё есть ссылки. Поэтому утечки возникают из-за логических ошибок в коде, когда ненужные объекты остаются на реферируемыми.

Основные причины утечек памяти

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

Одна из самых частых причин — накопление объектов в статических коллекциях:

public class UserCache {
    // Статический список растёт бесконечно
    private static List<User> userCache = new ArrayList<>();
    
    public static void addUser(User user) {
        userCache.add(user);
    }
}

Объекты в userCache никогда не удаляются, хотя приложение их больше не использует. GC не может удалить их, потому что есть статическая ссылка.

Решение:

public class UserCache {
    private static final int MAX_SIZE = 1000;
    private static LinkedHashMap<String, User> userCache = 
        new LinkedHashMap<String, User>(MAX_SIZE, 0.75f, true) {
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > MAX_SIZE; // LRU кэш
            }
        };
}

2. Слушатели и подписчики, которые не отписаны

public class UserListener implements EventListener {
    // ...
}

public class Application {
    public void init() {
        UserListener listener = new UserListener();
        eventBus.subscribe(listener); // Подписались
        // ... позже
        listener = null; // Но не отписались!
    }
}

Объект listener всё ещё на реферируется eventBus, поэтому GC не может его удалить.

Решение:

public void cleanup() {
    eventBus.unsubscribe(listener); // Явно отписываемся
    listener = null;
}

3. Неправильное использование потоков

public class ThreadLeakExample {
    private static final List<Thread> threads = new ArrayList<>();
    
    public void startWorkers() {
        for (int i = 0; i < 1000; i++) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(Long.MAX_VALUE); // Поток спит вечно
                } catch (InterruptedException e) {}
            });
            threads.add(t);
            t.start();
        }
    }
}

Потоки создаются, но никогда не завершаются. Они остаются в списке и консумируют память.

Решение: Использовать ExecutorService и корректно завершать потоки:

ExecutorService executor = Executors.newFixedThreadPool(10);
try {
    for (int i = 0; i < 1000; i++) {
        executor.submit(() -> { /* работа */ });
    }
} finally {
    executor.shutdown();
    executor.awaitTermination(60, TimeUnit.SECONDS);
}

4. Unclosed ресурсы (файлы, соединения)

public void readFile(String filename) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    String line = reader.readLine();
    System.out.println(line);
    // reader никогда не закрывается!
}

Отрытые потоки и соединения блокируют ресурсы и занимают память.

Решение: Try-with-resources:

public void readFile(String filename) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
        String line = reader.readLine();
        System.out.println(line);
    } // reader автоматически закроется
}

5. Циклические ссылки

public class Node {
    Node next;
    Object data;
}

public class LinkedList {
    private Node head;
    
    public void clear() {
        head = null; // Если next указывает обратно, может быть утечка
    }
}

Если есть циклические ссылки, GC может их очистить (современные GC это умеют), но старые GC имели проблемы.

6. WeakReference / SoftReference используются неправильно

public class Cache {
    // Неправильно — если не используется WeakHashMap
    private Map<Object, Object> cache = new HashMap<>();
    
    public void cache(Object key, Object value) {
        cache.put(key, value); // value останется в памяти
    }
}

Решение:

public class Cache {
    // Правильно — значения удалятся, когда GC их не найдёт
    private Map<Object, Object> cache = new WeakHashMap<>();
}

Как найти утечку памяти

  1. JProfiler или YourKit — профилировщики памяти
  2. JMeter — нагрузочное тестирование с анализом памяти
  3. jmap и jhat — стандартные инструменты JDK
jmap -dump:live,format=b,file=heap.bin <pid>
jhat heap.bin

Лучшие практики

  1. Закрывай все ресурсы (try-with-resources)
  2. Явно отписывайся от слушателей
  3. Избегай статических коллекций или ограничивай их размер
  4. Проверяй потоки — завершай их корректно
  5. Используй Weak/Soft References для кэшей
  6. Регулярно профилируй приложение

Утечки памяти — это обычно логические ошибки, а не баги в JVM. Внимательное проектирование и правильное управление жизненным циклом объектов предотвращают 99% утечек.