Почему этот вопрос задают на собеседовании
«Garbage collector при чистке проверяет, что объект имеет ссылку на один из корней. Что является "корнем"?» — вопрос, на который кандидаты обычно отвечают «статика и стек» и на этом останавливаются. А интервьюер ждёт понимания: откуда GC начинает обход и почему одни объекты умирают, а другие — нет.
Если вы хоть раз сталкивались с утечкой Activity, вы уже встречались с корнями — просто не знали этого. Понимание механизма работает в обе стороны: зная, что такое корни, легко объяснить, почему объект, про который «все давно забыли», всё ещё жив.
Разберём по шагам: определение, типичные корни, примеры с кодом и главный вывод для собеседования.
Что такое корень GC
Корни — это объекты и ссылки, которые GC считает заведомо живыми и с которых начинает обход графа объектов.
Если совсем просто: GC не перебирает все объекты случайно. Он берёт корневые ссылки и идёт по цепочкам ссылок. Всё, что достижимо из корней, считается живым. Что недостижимо — можно удалять.
Ключевая мысль: корень — это не сам объект в куче, а стартовая точка обхода. GC не ищет «плохие» объекты — он ищет заведомо хорошие, а всё остальное утилизирует.
Именно поэтому сборка мусора работает не с каждым объектом отдельно: один обход графа от корней решает, что живёт, а что нет.
Что обычно относится к корням
Интервьюеру достаточно услышать несколько категорий — но чем точнее, тем лучше:
- Живые локальные переменные и параметры методов в активных потоках — объекты, на которые смотрит текущий call stack
- Сами активные потоки — пока поток выполняется, его корневые ссылки живы
- Статические поля — живут, пока загружен класс, а класс — пока жив загрузчик
- Ссылки из JNI/native кода — то, что удерживает native-сторона
- Ссылки из системных загрузчиков классов и внутренних структур рантайма
Обратите внимание: «корни» — это не какая-то одна сущность. Это собирательное название для всех точек входа, с которых GC может стартовать обход.
Пример с локальной переменной
fun loadUser() {
val user = User()
// пока метод выполняется и user жив в стеке,
// объект User достижим из корня
}Пока метод выполняется, переменная user лежит в стеке потока — это и есть живая корневая ссылка. Как только метод завершился, ссылка исчезла, и объект можно собирать.
Пример со статикой
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 берёт корневые ссылки и идёт по графу — всё достижимое живёт, всё остальное удаляется. Покажите это — и вопрос закрыт.
