Сборка мусора в 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 — частый выбор для серверов, где важна предсказуемая низкая задержка ответа.

Проверьте себя
1. Как сборщик мусора в Go определяет, что объект можно удалить?
AСчитает количество ссылок на объект и удаляет, когда счётчик равен нулю
BПроверяет, достижим ли объект из корней (глобальные переменные, стеки горутин) — если нет, объект удаляется
CУдаляет объекты через фиксированный промежуток времени после создания
DПрограммист вручную вызывает функцию free() для каждого объекта
2. Почему сборщик мусора в современном Go считается «быстрым» для серверных приложений?
AОн вообще никогда не останавливает выполнение программы
BОн запускается только один раз при старте программы
CОсновная работа выполняется параллельно с программой, а полные остановки сведены к очень коротким паузам
DОн полностью отключён по умолчанию, и память освобождается только вручную