Этот вопрос проверяет понимание базового принципа работы GC: почему один объект удаляется, а другой остается, и как ссылки образуют «живой граф» объектов.
Объект считается достижимым, если до него можно добраться по цепочке ссылок от GC Roots.
GC начинает обход от корней и помечает все найденные объекты как живые.
Если объект не помечен, значит до него нельзя добраться из работающей программы.
Такие объекты считаются мусором и могут быть удалены.
GC определяет, какие объекты можно удалить, используя концепцию достижимости: в памяти рассматривается граф объектов, соединенных ссылками.
Достижимость объекта — это возможность попасть к нему по цепочке ссылок, начиная от GC Roots.
Идея простая:
Есть стартовые точки — GC Roots
От них GC проходит по ссылкам
Все посещенные объекты считаются живыми
Непосещенные — кандидаты на удаление
GC Roots — это ссылки, которые «принадлежат» работающей программе и считаются всегда живыми:
Локальные переменные и параметры методов в stack каждого потока
Активные объекты потоков (Thread)
Статические поля (static) загруженных классов
Ссылки из нативного кода (JNI), если они закреплены как корни
На уровне идеи это выглядит как «обход графа»:
GC собирает список GC Roots
Помечает объекты, на которые они указывают
Из каждого помеченного объекта идет дальше по ссылкам на другие объекты
Пока есть новые найденные объекты — продолжает обход
После этого:
Все помеченные объекты считаются живыми
Все остальные — мусор
Представь, что:
GC Roots — это «руки программы» (локальные переменные, статические поля)
Ссылки — это «веревочки» между объектами
Если от рук можно дотянуться до объекта по веревочкам — объект живой
Если нет — его можно убрать
Если два объекта ссылаются друг на друга, но на них нет ссылок от GC Roots, они оба будут удалены.
Пример:
class Node { Node next; }
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null; // потеряли корневую ссылку
b = null; // теперь оба недостижимы
Цикл есть, но достижимости от корней нет — значит, мусор.
Имеет значение не факт существования ссылок, а наличие пути от GC Roots.
Например, если объект лежит в коллекции, но сама коллекция больше не достижима, то и объект тоже недостижим.
Если положить объект в static поле, он становится достижимым от GC Roots через класс, и GC не сможет его удалить, пока класс загружен.
Пример:
public class CacheHolder {
static final List<byte[]> cache = new ArrayList<>();
}
Если cache растет бесконтрольно — это типичный путь к утечке памяти.
В Java утечка чаще всего означает не «память потерялась», а то, что:
Объекты больше не нужны
Но они все еще достижимы от GC Roots
Значит GC не имеет права их удалять
Типовые причины:
Долгоживущие коллекции (кэш без ограничений)
static ссылки на тяжелые объекты
Подписки/листенеры, которые не отписали
ThreadLocal, где значение не очистили
Достижимость определяется путём от GC Roots по цепочке ссылок.
Если путь есть — объект живой, если нет — GC может его удалить.
Чтобы избегать утечек, важно контролировать долгоживущие ссылки: static, коллекции, подписки и ThreadLocal.