1. Типичные ошибки при работе с памятью
Пора взглянуть на оборотную сторону магии автоматического управления памятью. Даже если вы не пишете на C, где за каждым байтом приходится следить лично, в Java можно «накосячить» так, что приложение будет жрать память, как прожорливый кот — колбасу. Давайте разберём самые распространённые ошибки и способы их избежать.
Забытые слушатели (listeners)
В Java часто используется паттерн «слушатель» — объект, который подписывается на события другого объекта. Например, вы создали кнопку и добавили к ней обработчик клика:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// обработка клика
}
});
Проблема: если вы забыли удалить этого слушателя (removeActionListener), когда кнопка или окно больше не нужны, слушатель остаётся висеть в памяти. Даже если вы закрыли окно и все ссылки на него обнулили, объект‑слушатель всё ещё держит ссылку на окно (или наоборот), не давая сборщику мусора освободить память.
Аналогия: Представьте, что вы переехали, но забыли отписаться от рассылки пиццерии — вам продолжают слать рекламу на старый адрес.
Статические коллекции, которые не очищаются
Статические поля живут столько же, сколько и класс (а иногда — до конца жизни приложения). Если у вас есть статическая коллекция:
public class Cache {
public static final List<String> globalList = new ArrayList<>();
}
и вы добавляете туда объекты, но не удаляете их, они будут висеть в памяти вечно. Даже если на сами объекты больше нигде нет ссылок, ссылка из статической коллекции не даст GC их убрать.
Реальный пример: Кэш фотографий в десктопном приложении, который никогда не чистится. Через пару часов работы — OutOfMemoryError.
Неосвобождение ресурсов (файлы, потоки, соединения)
Хотя Java и освобождает память, она не занимается автоматическим закрытием файловых дескрипторов, сетевых соединений и других внешних ресурсов. Если забыть закрыть файл или поток, ресурс останется висеть, и в какой‑то момент система скажет: «Всё, файлов больше не дам!» (IOException: Too many open files).
Совет: Всегда используйте try-with-resources:
try (FileInputStream in = new FileInputStream("data.txt")) {
// читаем файл
} // in.close() вызовется автоматически!
Большие объекты, которые долго висят в памяти
Иногда вы создаёте большой массив или коллекцию, используете её, а потом «забываете отпустить». Например:
List<byte[]> bigList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
bigList.add(new byte[1024 * 1024]); // 1 МБ каждый
}
// ... забыли очистить bigList
Если эта коллекция живёт в статическом поле или в объекте, который долго не удаляется, вся эта память останется занята.
Внутренние и анонимные классы: захват внешних ссылок
Анонимные (и внутренние) классы в Java хранят неявную ссылку на внешний объект:
public class Outer {
void doSomething() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello from inner!");
}
};
// r где-то сохраняется
}
}
Если объект r попадёт в статическую коллекцию или кэш, он будет «держать» ссылку на экземпляр Outer, даже если тот уже не нужен. В результате — утечка памяти. С лямбда‑выражениями ситуация чуть лучше, но если лямбда использует поля внешнего класса, ссылка всё равно сохраняется.
2. Ошибки при работе со сборщиком мусора
Принудительный вызов System.gc()
Многие новички думают: «Память заканчивается — вызову System.gc() и всё решится!». В действительности это всего лишь просьба к JVM, а не гарантия немедленной сборки. Частое использование может резко ухудшить производительность, вызвать длинные паузы и фризы. В реальных приложениях лучше доверять JVM — она сама решит, когда собирать мусор. Кстати, некоторые JVM могут игнорировать явные вызовы GC (например, с опцией -XX:+DisableExplicitGC).
Игнорирование логов GC
В логах GC видно, когда происходят сборки, сколько времени они занимают и сколько памяти освобождается. Если не смотреть эти логи, можно пропустить сигналы о проблемах: длинные паузы, частые Full GC, утечки памяти.
Как включить логи GC:
java -Xlog:gc* -jar MyApp.jar
или для старых JVM:
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar MyApp.jar
Неправильный выбор GC для задачи
Выбор сборщика влияет на задержки и стабильность. Для низкой задержки (биржи, онлайн‑игры) параллельный стоп‑мир Parallel GC — плохая идея: он может «замораживать» все потоки на время сборки. Рассмотрите G1 GC, ZGC или Shenandoah.
3. Ошибки с коллекциями
Использование HashMap вместо WeakHashMap для кэшей.
Если вы делаете кэш, где объекты должны автоматически удаляться, когда на них больше нет «живых» ссылок, используйте WeakHashMap:
Map<Key, Value> cache = new WeakHashMap<>();
С обычным HashMap объекты будут жить, пока кэш не очистится вручную, что приведёт к утечке памяти.
Забытые remove() для элементов.
Если вы добавляете объекты в коллекции (например, в списки слушателей), но не удаляете их, когда они больше не нужны, эти объекты будут жить вечно, особенно в долгоживущих коллекциях (например, статических).
4. Best practices: как избежать проблем
Всегда удаляйте слушателей.
Если объект подписался на события, обязательно отпишите его, когда он больше не нужен. Удобно делать это в методе dispose() или при закрытии окна/экрана.
button.removeActionListener(myListener);
Используйте слабые ссылки для кэшей.
Если в кэше можно обойтись без гарантии сохранения объекта, используйте WeakReference или коллекции на их основе (WeakHashMap). Так GC сможет освободить память, когда она понадобится.
Мониторьте память в продакшене.
Используйте jvisualvm, jconsole или APM‑системы. Это помогает поймать утечки до жалоб пользователей.
Анализируйте heap dump при подозрении на утечку
Если приложение стало «жрать» больше памяти, чем обычно, снимите heap dump (например, через jmap или jvisualvm) и посмотрите, какие объекты занимают больше всего места. Часто виновник находится за пару минут.
Настраивайте JVM‑параметры.
- -Xmx — максимальный размер кучи
- -Xms — начальный размер кучи
Разумные лимиты помогают избежать OutOfMemoryError и ускоряют диагностику.
5. Практика: пример кода с утечкой памяти и его исправление
Пример 1: Утечка через статическую коллекцию
public class MemoryLeakDemo {
// Статическая коллекция — живёт вечно
private static final List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
leakyList.add(new byte[1024 * 1024]); // 1 МБ каждый раз
System.out.println("Добавлено " + (i + 1) + " МБ");
}
// OutOfMemoryError!
}
}
Исправление: Используйте локальную переменную или очищайте коллекцию, когда она больше не нужна.
public class MemoryLeakFixed {
public static void main(String[] args) {
List<byte[]> tempList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
tempList.add(new byte[1024 * 1024]);
System.out.println("Добавлено " + (i + 1) + " МБ");
}
// tempList = null; // Можно явно обнулить
// Теперь объекты доступны для GC после выхода из метода
}
}
Пример 2: Утечка через слушателя
public class Window {
private final List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener l) {
listeners.add(l);
}
// Нет метода removeListener!
}
Исправление: Добавьте метод для удаления слушателя и вызывайте его при закрытии окна.
public void removeListener(EventListener l) {
listeners.remove(l);
}
Пример 3: Кэш с HashMap вместо WeakHashMap
Map<Object, Object> cache = new HashMap<>();
// ... добавляем объекты
Исправление: Перейдите на WeakHashMap:
Map<Object, Object> cache = new WeakHashMap<>();
Советы по настройке JVM для мониторинга памяти
- Включайте GC‑логи: -Xlog:gc* или -XX:+PrintGCDetails
- Ограничьте максимальный размер кучи: -Xmx512m
- Если используете кэши — следите за их размером и применяйте слабые ссылки, если это допустимо
- Экспериментируйте с GC: -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseShenandoahGC
7. Типичные ошибки при работе с памятью
Ошибка №1: Забытые слушатели и подписки. Если вы добавили слушателя к объекту, но забыли его удалить, объект‑слушатель (и всё, на что он ссылается) останется в памяти. Классика для GUI и событийных систем. Используйте removeListener/removeActionListener.
Ошибка №2: Статические коллекции без очистки. Статические поля живут дольше всего. Если вы кладёте в них объекты и не чистите коллекцию, эти объекты останутся в памяти навсегда. Особенно коварно для безразмерных кэшей.
Ошибка №3: Неосвобождение внешних ресурсов. Оставили открытым поток, файл или соединение? Вы теряете память и упираетесь в лимиты ОС. Используйте try-with-resources и закрывайте ресурсы.
Ошибка №4: Принудительный вызов System.gc(). Это не панацея, а всего лишь просьба к JVM. Часто приводит к паузам и деградации производительности.
Ошибка №5: Обычные коллекции для кэшей. Если объекты в кэше должны удаляться сами, применяйте слабые/soft‑ссылки (WeakHashMap, SoftReference). Иначе получите утечку.
Ошибка №6: Внутренние и анонимные классы, захватывающие внешние ссылки. Внутренние классы и лямбды могут неявно удерживать ссылку на внешний объект. Если их сохранить в долгоживущей коллекции — это утечка.
Ошибка №7: Игнорирование логов GC. Если вы не смотрите логи GC, вы не узнаете про длинные паузы или частые Full GC — а пользователи узнают по тормозам и фризам. Включайте -Xlog:gc* или -XX:+PrintGCDetails.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ