Сборщик мусора и GOGC

О лекции

Сборщик в Go помечает живые объекты параллельно с программой. Полной остановки мира на всю кучу нет, но короткие паузы есть. GOGC задаёт, насколько куча может вырасти относительно живых данных между циклами.

Рантайм и инструменты Go golang собеседование

Память на куче забирает сборщик, когда объекты недостижимы из корней: стеки горутин, глобальные, регистры. Программист не вызывает free. Цена — CPU на обход указателей и редкие короткие паузы, когда мир всё же останавливают. Это не «в Go нет пауз».

На бэкенде сборщик виден как CPU и как хвост задержки под потоком аллокаций. Утечка горутин, глобальный кэш без вытеснения, огромный буфер через подстроку — живые объекты. Сборщик их не заберёт. Крутить GOGC бесполезно, пока живой граф растёт без границы.

Аналогия: уборщик ходит по офису, пока люди работают, и помечает столы, до которых можно дотянуться по цепочке «кто на кого ссылается». На секунду просит всех замереть — сверить список. Бумажки на столах, которые кто-то всё ещё держит, он не выкинет.

Цикл и паузы

Короткие остановки мира нужны на старте и конце цикла. Основная пометка живых объектов идёт параллельно. Паузы обычно миллисекунды и меньше на типичных кучах, но не ноль. CPU на сборку есть всегда, когда идёт цикл.

Три цвета — способ не собрать живой объект по ошибке. Белые ещё не посещены, серые в очереди, чёрные живые с уже просмотренными детьми. Пока программа пишет указатели параллельно с обходом, барьер записи не даёт спрятать указатель из чёрного в белый без учёта.

// GOGC=100 по умолчанию: цель ~2× live после GC
// GOGC=200 — реже циклы, больше RSS
// GOGC=off — только runtime.GC() вручную, почти никогда в проде
//
// GOMEMLIMIT (Go 1.19+): мягкий предел памяти процесса
//   GC чаще, меньше шанс OOM, больше CPU на сборке
//
// runtime.ReadMemStats / pprof heap — кто держит live

GOGC=100: после цикла куча может вырасти примерно на 100% живого. GOMEMLIMIT — мягкий потолок.

Пример. Сервис держит в глобальной map все когда-либо виденные сессии. Сборщик честный: на сессии есть ссылка, память не отдаст. GOGC=50 только чаще будет обходить эту же map. Нужен TTL или delete, не тюнинг.

GOGC и GOMEMLIMIT

GOGC=100 значит: после сборки куча может примерно удвоиться относительно живых данных, затем новый цикл. Больше GOGC — реже сборка, выше пик RSS. Меньше — чаще сборка, ниже пик, больше CPU. GOGC=off отключает автоматику: только ручной runtime.GC(), для прода почти никогда.

GOMEMLIMIT (Go 1.19+) задаёт мягкий предел: сборщик старается уложиться, даже ценой чаще крутить циклы. Это снижает риск OOM в контейнере, но не лечит утечку. Лимит памяти пода бьёт по RSS процесса.

Смотреть, кто держит память, нужно профилем heap, не спором про GOGC. Живые горутины, кэш, map без delete — корни. Тюнинг без профиля — гадание.

Типичная ошибка

Обещать «в Go нет пауз сборщика». Есть короткие остановки мира и постоянный налог CPU. Крутить GOGC, не закрыв утечку горутин. Путать GOMEMLIMIT с жёсткой гарантией задержки. Считать, что сборщик заберёт объекты, на которые ещё смотрит глобальная map.

На собеседовании

Пометка живых идёт параллельно, паузы короткие. GOGC — цель роста кучи относительно живых данных, не «100 мс пауза». GOMEMLIMIT — мягкий потолок против OOM. Сначала pprof heap: кто живой, потом тюнинг.