Почему этот вопрос задают на собеседовании

«Garbage collector при чистке проверяет, что объект имеет ссылку на один из корней. Что является "корнем"?» — вопрос, на который кандидаты обычно отвечают «статика и стек» и на этом останавливаются. А интервьюер ждёт понимания: откуда GC начинает обход и почему одни объекты умирают, а другие — нет.

Если вы хоть раз сталкивались с утечкой Activity, вы уже встречались с корнями — просто не знали этого. Понимание механизма работает в обе стороны: зная, что такое корни, легко объяснить, почему объект, про который «все давно забыли», всё ещё жив.

Разберём по шагам: определение, типичные корни, примеры с кодом и главный вывод для собеседования.

Что такое корень GC

Корни — это объекты и ссылки, которые GC считает заведомо живыми и с которых начинает обход графа объектов.

Если совсем просто: GC не перебирает все объекты случайно. Он берёт корневые ссылки и идёт по цепочкам ссылок. Всё, что достижимо из корней, считается живым. Что недостижимо — можно удалять.

Ключевая мысль: корень — это не сам объект в куче, а стартовая точка обхода. GC не ищет «плохие» объекты — он ищет заведомо хорошие, а всё остальное утилизирует.

Именно поэтому сборка мусора работает не с каждым объектом отдельно: один обход графа от корней решает, что живёт, а что нет.

Что обычно относится к корням

Интервьюеру достаточно услышать несколько категорий — но чем точнее, тем лучше:

  • Живые локальные переменные и параметры методов в активных потоках — объекты, на которые смотрит текущий call stack
  • Сами активные потоки — пока поток выполняется, его корневые ссылки живы
  • Статические поля — живут, пока загружен класс, а класс — пока жив загрузчик
  • Ссылки из JNI/native кода — то, что удерживает native-сторона
  • Ссылки из системных загрузчиков классов и внутренних структур рантайма

Обратите внимание: «корни» — это не какая-то одна сущность. Это собирательное название для всех точек входа, с которых GC может стартовать обход.

Пример с локальной переменной

MainActivity.kt
fun loadUser() {
    val user = User()
    // пока метод выполняется и user жив в стеке,
    // объект User достижим из корня
}

Пока метод выполняется, переменная user лежит в стеке потока — это и есть живая корневая ссылка. Как только метод завершился, ссылка исчезла, и объект можно собирать.

Пример со статикой

UserCache.kt
object UserCache {
    var user: User? = null
}

Если статическое поле держит объект, объект будет оставаться живым, пока на него есть такая ссылка. Статическое поле — это корень, поэтому GC каждый раз будет начинать обход с него и «дотягиваться» до объекта.

Почему статика опаснее локальной переменной

Локальная переменная живёт, пока выполняется метод, — утечка «лечится» сама. Статическое поле живёт столько, сколько жив класс: всё время жизни приложения. Поэтому неаккуратная статика — главный источник утечек.

Главное: не нужна прямая ссылка на корень

Тут важно: объекту не обязательно иметь прямую ссылку на корень. Достаточно цепочки ссылок от корня.

thread -> local variable -> viewModel -> repository -> user

Если thread и local variable живы, то user тоже достижим — GC дойдёт до него по цепочке, даже если сам объект «ничего не знает» о корнях.

Поэтому на собеседовании хороший ответ — не просто «статика и стек», а именно понимание: корни — это стартовые достижимые ссылки, с которых GC начинает обход графа.

Откуда берутся утечки

Из этого же следует популярный источник утечек: если длинноживущий корень держит ссылку на Activity или Context, GC не сможет собрать этот экран.

Классика жанра

Singleton хранит listener со ссылкой на Activity. Сам singleton жив, listener жив, значит, и Activity остаётся достижимым — и утечёт вместе со всем своим view-деревом.

Дальше — цепная реакция: Activity удерживает layout, layout — все view, а через ViewModel и репозиторий — весь граф объектов экрана. Один невинный listener — и десятки мегабайт живут до конца приложения.

Поэтому на практике утечки чинят двумя способами: обрывают цепочку (убирают listener в onDestroy) или делают ссылку слабой (WeakReference), чтобы она не участвовала в обходе как живая.

Как звучит сильный ответ на собеседовании

«Корни — это объекты и ссылки, которые GC считает заведомо живыми и с которых начинает обход графа. Это локальные переменные и параметры методов в активных потоках, сами потоки, статические поля, ссылки из JNI и внутренних структур рантайма. Объекту не обязательно иметь прямую ссылку на корень — достаточно цепочки ссылок. Всё, что достижимо из корней, живёт, остальное GC собирает. Отсюда и утечки: если длинноживущий корень вроде singleton держит ссылку на Activity, экран останется достижимым, и GC его не тронет.»

Собеседования на Android-разработчика

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

Итоги

Корни — это не абстракция из учебника, а вполне практичная вещь: зная их, легко понять, откуда берутся утечки и почему объект не собирается, хотя про него все давно забыли.

Три вещи, которые стоит унести с собой:

  • Корни — стартовые точки обхода: локальные переменные и параметры в активных потоках, сами потоки, статические поля, JNI-ссылки.
  • Достаточно цепочки ссылок — прямой связи с корнем не нужно; достижимость решает всё.
  • Утечки — это длинноживущий корень, держащий ссылку на короткоживущий объект: singleton + listener + Activity.

Главное, что нужно запомнить

На собеседовании от вас ждут не перечисления «статика и стек», а понимания механизма: GC берёт корневые ссылки и идёт по графу — всё достижимое живёт, всё остальное удаляется. Покажите это — и вопрос закрыт.