Сборка мусора в Go — невидимая, но не бесплатная
Go даёт вам автоматическое управление памятью, не отнимая указателей: вы пишете &Point{}, получаете обычный адрес и забываете про free. Но за этим удобством стоит конкурентный сборщик мусора, который работает прямо рядом с вашим кодом — на тех же ядрах, в те же миллисекунды. «Не думать о памяти» в Go означает «не думать, пока всё хорошо». Когда всё плохо — растёт RSS, скачет p99, процесс OOM-killed — отлаживать сборщик придётся именно вам.
Главное, что отличает GC Go от Java или .NET: он не поколенческий и не уплотняющий. Объекты никогда не перемещаются, поэтому адрес стабилен и взятие указателя на поле структуры безопасно — но именно поэтому нет «дешёвой малой сборки молодого поколения». Сборщик трёхцветный, mark-and-sweep, и почти весь свой цикл выполняется конкурентно с программой: за цикл всего две короткие паузы stop-the-world, каждая обычно меньше миллисекунды. Ловушки начинаются там, где интуиция из других рантаймов даёт сбой: GOGC — это не интервал времени, а процент роста кучи; runtime.GC() блокирует, а не запускает фоновую сборку; GOMEMLIMIT — это мягкая цель, а не жёсткий лимит с паникой. Эта тема разбирает сборщик по слоям — от устройства цикла до тонкого тюнинга.
Карта темы
- Какой сборщик у Go — конкурентный трёхцветный mark-and-sweep, без поколений и без уплотнения, объекты не двигаются.
- Триггер цикла GC — цикл стартует от роста кучи на
GOGCпроцентов, отGOMEMLIMIT, поruntime.GC()или раз в две минуты. - Пейсер GC — как пейсер решает, когда начать цикл, и заставляет аллокацию не обгонять mark через mark assist.
- Паузы stop-the-world — две короткие STW-паузы за цикл, на старте и завершении mark, каждая обычно субмиллисекундная.
- Конкурентная пометка — фоновые mark-воркеры сканируют кучу, пока программа работает; mark assist держит аллокацию в узде.
- Трёхцветная пометка — белый, серый, чёрный и инвариант, по которому чёрный объект не ссылается на белый.
- Барьер записи — гибридный барьер Дейкстры-Юаса перехватывает запись указателей и сохраняет инвариант при конкурентном mark.
- Тюнинг GC —
GOGCдля баланса задержки и пропускной способности,GOMEMLIMITвместо устаревшего трюкаballast.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Называть GC Go поколенческим | Неверная модель; в Go нет молодого и старого поколений |
| Считать, что сборщик уплотняет кучу и двигает объекты | Ложный вывод, что указатели Go нестабильны |
| Путать mark-and-sweep с подсчётом ссылок | Не объяснить циклические ссылки и фоновый цикл |
Считать GOGC интервалом времени | GOGC — процент роста кучи, а не миллисекунды |
| Думать, что GC идёт по фиксированному таймеру | Сборка управляется аллокациями, не часами |
Считать runtime.GC() асинхронным | Он блокирует вызвавшую goroutine до конца цикла |
| Думать, что STW длится всю фазу mark | На деле две короткие паузы на границах цикла |
| Полагать, что пауза растёт с размером кучи | В современном Go STW почти не зависит от размера живой кучи |
| Путать барьер записи с барьером чтения | Go использует барьер записи, не читающий хук |
| Считать mark assist отдельной STW-паузой | Это inline-работа, которую делает сама аллоцирующая goroutine |
Считать GOMEMLIMIT жёстким лимитом с паникой | Это мягкая цель пейсинга; при превышении GC лишь агрессивнее |
Советовать ballast на современном Go | GOMEMLIMIT решает ту же задачу честнее |
Значение для собеседований
Сборка мусора — обязательная тема на любом senior-уровне Go-интервью. Интервьюер проверяет не знание слова «mark-and-sweep», а наличие рабочей модели: что сборщик делает под капотом и как его поведением управлять.
Что обычно проверяют:
- Какой сборщик у Go и какие у него определяющие свойства — конкурентный, трёхцветный, без поколений, без уплотнения.
- Что запускает цикл GC — рост кучи на
GOGCпроцентов,GOMEMLIMIT,runtime.GC(), форс раз в две минуты. - Что решает пейсер — момент старта цикла и долг mark assist, а не число потоков сборщика.
- Когда возникают STW-паузы — две короткие, на старте и завершении mark, и почему они почти не зависят от размера кучи.
- Зачем конкурентному mark нужен барьер записи и какой инвариант он сохраняет.
- Как
GOGCиGOMEMLIMITвлияют на баланс задержки, пропускной способности и памяти.
Типичный неверный ответ: «GOGC — это сколько процентов RAM можно занять, а runtime.GC() запускает сборку в фоне». Это запускает разбор того, что GOGC — процент роста кучи относительно живых данных, а runtime.GC() синхронно блокирует вызвавшую goroutine до завершения полного цикла.