Сборка мусора в Go простыми словами
Вопрос с собеседования: «Объясните, как Go понимает, что объект в памяти больше не нужен, и когда он его удаляет?»
Звучит просто, но за этим вопросом стоит целая подсистема рантайма Go — сборщик мусора (garbage collector, или сокращённо GC). Давайте разберёмся, что это такое и почему интервьюеры так любят про него спрашивать.
Зачем вообще нужна сборка мусора
Когда программа создаёт данные — структуру, срез, объект через new — эти данные где-то хранятся в памяти компьютера. В языках вроде C программист сам обязан помнить: «я создал этот объект, значит, я же должен и удалить его, когда он не нужен». Забыл удалить — память утекает, программа со временем съедает всю оперативку. Удалил слишком рано, пока объект ещё используется — программа падает или выдаёт мусор.
Go берёт эту головную боль на себя. Программист просто создаёт объекты, а специальный фоновый механизм — сборщик мусора — сам следит, какие объекты ещё используются, а какие уже никому не нужны, и освобождает память от вторых.
Посмотрим на пример:
func createUser() *User {
u := &User{Name: "Аня"}
return u
}
func main() {
user := createUser()
fmt.Println(user.Name)
user = nil // теперь на объект User никто не ссылается
}Вывод:
АняПосле строки user = nil объект User, который мы создали, становится «сиротой» — на него больше никто не ссылается из кода программы. Именно такие объекты и находит сборщик мусора, чтобы освободить память под них.
Как GC понимает, что объект «мусор»
Главная идея простая: объект жив, пока на него есть хоть одна ссылка из «живой» части программы — из глобальных переменных, из стека выполняющихся функций, из других живых объектов. Если ни одной ссылки не осталось — объект можно смело удалять, до него всё равно никто не доберётся.
Формально это называется достижимостью (reachability). GC не считает ссылки, как в некоторых других языках (это называется reference counting) — вместо этого он периодически обходит граф объектов, начиная от «корней» (global-переменные, локальные переменные в стеках горутин), и помечает всё, до чего смог дойти, как живое. То, что не пометил, — мусор, его можно убирать.
Как это работает под капотом
У Go нестандартный, но очень продуманный алгоритм GC — concurrent mark-and-sweep (параллельная пометка-и-очистка). Разберём по шагам:
1. Mark (пометка). Сборщик мусора обходит все достижимые объекты, начиная от корней, и ставит на них «метку живой». Здесь и кроется главная фишка Go: эта фаза выполняется параллельно с работой вашей программы, а не останавливает её полностью. Пока GC ходит по объектам, ваши горутины продолжают выполняться (с небольшими короткими паузами, о которых ниже).
2. Sweep (очистка). Всё, что осталось непомеченным после mark-фазы, считается мусором — эта память возвращается в пул, откуда рантайм Go сможет выделить её под новые объекты.
Ключевое слово тут — concurrent, «параллельный». В старых версиях Go (и во многих других языках с GC) сборка мусора работала по принципу «stop-the-world»: вся программа полностью останавливалась на время сборки, что могло занимать десятки или сотни миллисекунд. Для веб-сервера, который должен отвечать за миллисекунды, это катастрофа.
Go решил эту проблему: с версии 1.5 сборщик мусора работает почти полностью параллельно с программой. Полная остановка («stop-the-world») всё ещё происходит, но она сведена к двум очень коротким паузам — в начале и в конце цикла GC, каждая обычно меньше 0.5 миллисекунды. Именно поэтому в резюме и на собеседованиях про Go часто говорят: «низкие паузы GC» — это одна из ключевых причин, почему Go выбирают для высоконагруженных сетевых сервисов.
Когда запускается сборка мусора
GC в Go запускается не по расписанию «раз в секунду», а по достижении определённого порога роста используемой памяти. По умолчанию Go запускает новый цикл сборки, когда объём «живых» данных в куче вырастает примерно вдвое с момента предыдущей сборки (это поведение настраивается через переменную окружения GOGC). Это баланс между двумя крайностями: собирать мусор слишком часто — тратить процессорное время впустую; собирать слишком редко — раздувать потребление памяти.
Частые ошибки на собеседовании
Кандидаты часто путают несколько вещей, и это сразу заметно опытному интервьюеру:
- «Go считает ссылки, как Python» — нет, в Go нет подсчёта ссылок (reference counting), используется именно обход графа объектов от корней (mark-and-sweep).
- «GC полностью останавливает программу» — это верно для старых версий и для многих других языков, но в современном Go остановка сведена к очень коротким паузам, основная работа идёт параллельно.
- «Раз есть GC, о памяти можно не думать вообще» — GC избавляет от ручного
free(), но не от логических утечек: если вы держите глобальный срез, который бесконтрольно растёт, или забыли отписаться от канала, GC не сможет освободить эту память, потому что формально на неё всё ещё есть ссылка. - Путают GC с escape analysis — это разные механизмы. GC решает, когда удалить объект из кучи. А то, окажется ли объект в куче вообще (а не на стеке) — решает другой механизм, escape analysis, о котором поговорим в следующем уроке.
Итоги-шпаргалка
Сборщик мусора в Go — это фоновый механизм, который сам находит и освобождает память объектов, до которых больше нет ссылок из работающей программы. Работает по алгоритму mark-and-sweep: сначала помечает все достижимые от корней объекты как живые, затем очищает всё непомеченное. Главная особенность Go — эта работа выполняется параллельно с программой, а не останавливает её целиком, паузы сведены до долей миллисекунды. Именно поэтому Go — частый выбор для серверов, где важна предсказуемая низкая задержка ответа.