Как работает сборщик мусора в Go?
Что проверяют
Понимаете ли вы, что сборщик конкурентный и настроен на короткие паузы, и знаете ли, чем на него можно влиять.
Как отвечать
В Go сборщик отслеживающий, конкурентный, с трёхцветной пометкой и барьером записи. Поколений нет, уплотнения тоже: объекты не переезжают.
Как проходит цикл: короткая остановка мира для подготовки, затем пометка живых объектов параллельно с работой программы, ещё одна короткая остановка для завершения пометки, и уборка. Барьер записи нужен, чтобы программа, меняющая ссылки во время пометки, не спрятала живой объект.
Ключевая цель разработчиков это паузы: они держатся в пределах долей миллисекунды и почти не зависят от размера кучи. Платой служит пропускная способность, часть процессорного времени уходит на сборку.
Чем можно влиять:
- GOGC: процент прироста кучи, после которого запускается цикл. По умолчанию 100, то есть удвоение. Больше значение, реже сборки и больше памяти;
- GOMEMLIMIT (с версии 1.19): мягкий предел памяти, ниже которого сборщик старается удержаться. Особенно полезен в контейнерах с лимитом;
- меньше мусора: переиспользование буферов через
sync.Pool, предвыделение слайсов, отказ от лишних преобразований в строки.
Полезно упомянуть анализ побега: компилятор решает, попадёт ли объект в кучу или останется на стеке, и посмотреть это можно через go build -gcflags=-m. Что не попало в кучу, сборщик не увидит вовсе.
Где спотыкаются
Говорят про поколения, которых в Go нет.
Считают, что runtime.GC() нужен в обычном коде.
Не знают про GOMEMLIMIT и ловят убийство контейнера по памяти.
Забывают, что утечки возможны и здесь: живая ссылка в глобальной карте или подвисшая горутина держат объекты вечно.