>>
Что такое утечка памяти в Java и как её создать?
Утечка памяти (memory leak) в Java — это ситуация, когда объекты больше не используются программой, но сборщик мусора не может их удалить, потому что на них всё ещё есть ссылки из "живых" частей приложения. В результате память постепенно заполняется, и рано или поздно вы получите OutOfMemoryError. В отличие от простого переполнения (как в вашем примере с бесконечным циклом), утечка обычно происходит незаметно и накапливается со временем.
Вот несколько классических примеров утечек памяти в Java с объяснениями.
1. Неправильное использование статических коллекций
Самый частый случай. Если вы храните объекты в статическом List, Map или другом контейнере, эти объекты никогда не будут удалены сборщиком мусора, пока жив класс (а класс жив, пока жив ClassLoader, обычно — всё время работы приложения).
import java.util.HashMap;
import java.util.Map;
public class CacheLeak {
private static Map<String, UserSession> sessions = new HashMap<>();
public static void addSession(String id, UserSession session) {
sessions.put(id, session);
}
// Нет метода removeSession() — утечка!
}
Решение: Всегда удаляйте объекты из коллекций, когда они больше не нужны. Используйте WeakHashMap или кэши с ограничением по размеру/времени (например, LinkedHashMap с removeEldestEntry()).
2. Неотменённые слушатели и колбэки
Вы регистрируете слушатель (например, в GUI или в системе событий), но забываете его отменить. Даже если объект-слушатель и объект-источник событий больше не нужны, ссылка из источника на слушатель не даёт GC удалить слушателя.
public class Button {
private List<ClickListener> listeners = new ArrayList<>();
public void addListener(ClickListener listener) { listeners.add(listener); }
// Нет removeListener() — утечка!
}
public class MyWindow {
private Button button;
public MyWindow() {
button = new Button();
button.addListener(event -> System.out.println("Clicked"));
}
// Когда окно закрывается, объект MyWindow может быть собран,
// но лямбда-слушатель (анонимный класс) всё ещё хранится в Button,
// который может быть статическим или долгоживущим.
}
Решение: Всегда отменяйте регистрацию слушателей в методе dispose() или cleanup(). Используйте слабые ссылки (WeakReference) в списке слушателей.
3. Внутренние классы, захватывающие внешний класс
Нестатический внутренний класс (inner class) содержит неявную ссылку на экземпляр внешнего класса. Если вы создаёте объект внутреннего класса и передаёте его куда-то, внешний класс не может быть собран GC, пока жив внутренний класс.
public class Outer {
private int[] bigData = new int[1000000]; // Много памяти
public class Inner {
public void doSomething() { }
}
public Inner getInner() {
return new Inner(); // Inner хранит ссылку на Outer
}
}
// Где-то в коде:
Outer outer = new Outer();
Outer.Inner inner = outer.getInner();
outer = null; // Outer не будет собран GC, потому что inner всё ещё жив!
Решение: Если внутреннему классу не нужен доступ к полям внешнего, сделайте его статическим (static inner class). Статические внутренние классы не имеют ссылки на внешний.
4. Потоки (Thread) и ThreadLocal
Каждый поток имеет свой ThreadLocal — карту, где ключом является сам объект ThreadLocal, а значением — данные. Если вы используете ThreadLocal в пуле потоков (например, в веб-сервере), и не удаляете данные после обработки запроса, они будут висеть в памяти до завершения потока. А потоки в пуле живут долго.
public class RequestHandler {
private static ThreadLocal<UserContext> userContext = new ThreadLocal<>();
public void handleRequest(User user) {
userContext.set(new UserContext(user)); // Данные привязаны к потоку
// ... обрабатываем запрос ...
// Забыли вызвать userContext.remove() — утечка!
}
}
Решение: Всегда вызывайте ThreadLocal.remove() после того, как данные больше не нужны, особенно в пуле потоков. Лучше всего — в блоке try/finally.
5. Класс-загрузчики (ClassLoader)
Это экзотическая, но серьёзная утечка. Если вы динамически загружаете классы (например, в серверах приложений или при горячей перезагрузке кода), и какой-то объект держит ссылку на класс или его статическое поле, то весь ClassLoader (а с ним и все загруженные классы) не может быть собран GC. Это приводит к PermGen / Metaspace утечкам.
Вывод: Утечка памяти в Java — это всегда про "забытую ссылку". Чтобы её избежать, следите за жизненным циклом объектов, используйте слабые ссылки (WeakReference, SoftReference) для кэшей, и всегда очищайте ресурсы в finally блоках или с помощью try-with-resources.
<<>>
Альтернативный взгляд: утечки через нативные ресурсы и I/O
Помимо классических Java-утечек, описанных выше, существует ещё один важный класс проблем — утечки нативных ресурсов. Хотя формально это не утечка памяти в куче Java, она приводит к тому же результату: приложение перестаёт работать или падает с ошибкой.
Рассмотрим пример с незакрытыми потоками (InputStream, OutputStream, Socket, Connection к БД):
public void readFile(String path) {
FileInputStream fis = new FileInputStream(path);
// Читаем файл...
// Забыли вызвать fis.close() — утечка дескриптора файла!
}
Каждый открытый файл или сокет использует нативный дескриптор (file descriptor). Операционная система имеет лимит на количество открытых дескрипторов. Если вы будете открывать файлы и не закрывать их, рано или поздно вы исчерпаете лимит, и новые операции ввода-вывода станут невозможны. Это тоже своего рода "утечка памяти", только на уровне ОС.
Решение: Всегда используйте try-with-resources (Java 7+), который автоматически закрывает ресурсы, реализующие AutoCloseable:
try (FileInputStream fis = new FileInputStream(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
// Читаем файл...
} // fis и reader автоматически закроются здесь
Также стоит упомянуть DirectByteBuffer — буферы, выделенные вне кучи Java (native memory). Если вы создаёте много таких буферов и не освобождаете их, вы можете получить OutOfMemoryError, хотя куча Java будет почти пуста. Отслеживать такие утечки сложнее, для этого нужны специальные профилировщики (например, JProfiler, VisualVM с плагином).