Сборщик мусора: как JVM освобождает память

Разбираем, почему в Java не нужно вручную освобождать память — и как устроен механизм, который делает это за вас.

Сборщик мусора (Garbage Collector, GC) — часть JVM, которая автоматически находит объекты, на которые больше никто не ссылается, и освобождает занятую ими память.

Что выведет этот код?

Начнём с примера, который заставляет задуматься о том, что вообще происходит с памятью в Java:

public class Main {
    public static void main(String[] args) {
        String text = new String("Привет");
        System.out.println(text);
        text = null;
        System.out.println("Ссылка обнулена, но программа работает дальше");
    }
}

Вывод:

Привет
Ссылка обнулена, но программа работает дальше

Ничего страшного не происходит — программа просто продолжает работать. Но вопрос на собеседовании звучит иначе: «Что произошло с объектом String, который раньше хранился в text, после строки text = null?» И вот тут начинается самое интересное.

Почему в Java не бывает free() и delete

Если вы хоть немного знакомы с C или C++, то знаете, что там программист сам обязан освобождать память, которую выделил — через free() или delete. Забыл освободить — получил утечку памяти. Освободил дважды или слишком рано — получил крах программы.

В Java такого нет вообще. Разработчик создаёт объекты через new, но никогда не пишет код вида «удали этот объект». Этим занимается сборщик мусора — отдельный механизм внутри JVM, который работает в фоне, параллельно с вашей программой.

Когда объект становится «мусором»

Объект в Java становится кандидатом на удаление, когда на него больше не осталось ни одной живой ссылки из работающей части программы. Это называется «объект недостижим» (unreachable).

public class Main {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder("данные");
        System.out.println(builder);
        builder = null;
        // теперь объект StringBuilder("данные") недостижим —
        // на него не осталось ни одной ссылки, GC может его удалить
        System.gc(); // «просьба» к JVM запустить сборку мусора
    }
}

Важная деталь: System.gc() — это не команда «удали прямо сейчас», а лишь просьба к JVM. Сборщик мусора сам решает, когда именно запускаться — сразу, чуть позже или вообще проигнорировать вызов, если посчитает, что памяти достаточно. Разработчик не контролирует момент удаления напрямую, и это специально: JVM видит общую картину использования памяти лучше, чем отдельный кусок кода.

Разберём ещё один пример — объект может стать недостижимым не только через null, но и когда переменная просто выходит из области видимости:

public class Main {
    static void createObject() {
        StringBuilder local = new StringBuilder("временный");
        System.out.println(local);
    } // здесь метод завершается, переменная local исчезает,
      // объект StringBuilder("временный") становится недостижимым

    public static void main(String[] args) {
        createObject();
        System.out.println("Метод завершён, объект внутри него уже недостижим");
    }
}

Вывод:

временный
Метод завершён, объект внутри него уже недостижим

Как это работает под капотом: идея поколений

Самое интересное — как именно JVM ищет объекты-мусор, не тормозя при этом всю программу. Один из ключевых приёмов называется generational garbage collection (сборка мусора по поколениям), и в его основе лежит простое наблюдение из практики: большинство объектов «умирают молодыми». Временные переменные, промежуточные строки, объекты внутри одного метода — почти все они становятся мусором очень быстро. А вот объекты, которые пережили много циклов сборки, обычно продолжают жить ещё долго (например, кеши или списки, созданные при старте программы).

Исходя из этого, память в куче JVM (heap) делится на зоны:

ЗонаЧто там хранится
Young Generation (молодое поколение)только что созданные объекты — сюда попадает почти всё
Old Generation (старое поколение)объекты, которые «пережили» несколько сборок мусора в молодом поколении

Логика простая: сборка мусора в Young Generation происходит часто, но быстро — там объектов много, но проверять их дёшево, потому что зона небольшая. Когда объект переживает несколько таких проверок подряд (то есть на него всё ещё есть ссылки), JVM «повышает» его в Old Generation. А там сборка мусора запускается гораздо реже, потому что объекты, дожившие до старого поколения, с высокой вероятностью проживут ещё долго — незачем проверять их постоянно.

Благодаря такому разделению JVM не обязана сканировать всю память при каждой проверке — это и делает автоматическое управление памятью в Java достаточно быстрым для промышленной разработки.

Частые ошибки на собеседовании

  • Говорят, что System.gc() гарантированно немедленно запускает сборку мусора. Это лишь рекомендация JVM, а не команда.
  • Путают присвоение null с реальным удалением объекта — null лишь обнуляет ссылку, а сам объект удалится позже, когда до него дойдёт очередь у сборщика.
  • Не могут объяснить, зачем нужно деление на поколения, и говорят просто «для оптимизации» без понимания сути: большинство объектов живут очень недолго, и с этим наблюдением выгодно работать отдельно.
  • Считают, что в Java невозможны утечки памяти. Возможны — например, если объект случайно остаётся достижимым (лежит в статической коллекции, из которой его забыли удалить), сборщик мусора его не тронет, даже если он фактически больше не нужен программе.

Итоги-шпаргалка

  • В Java не нужно вручную освобождать память — этим занимается сборщик мусора внутри JVM.
  • Объект становится кандидатом на удаление, когда на него не остаётся ни одной живой ссылки (объект «недостижим»).
  • System.gc() — лишь просьба к JVM, а не гарантия немедленной сборки мусора.
  • Куча делится на Young Generation (новые объекты, частая быстрая сборка) и Old Generation (долгоживущие объекты, редкая сборка) — это ускоряет работу GC.
  • Утечки памяти в Java всё же возможны, если объект случайно остаётся достижимым дольше, чем нужно.
Проверьте себя
1. Когда объект в Java становится кандидатом на удаление сборщиком мусора?
AСразу после выхода из блока кода, где он создан
BКогда на него не остаётся ни одной живой ссылки
CТолько после явного вызова delete
DЧерез фиксированный промежуток времени после создания
2. В чём смысл деления кучи на Young Generation и Old Generation?
AYoung Generation — для примитивов, Old Generation — для объектов
BЭто ускоряет сборку мусора, так как большинство объектов умирают молодыми
COld Generation используется только для статических переменных
DЭто чисто историческое разделение без практического смысла