За счёт чего сборщик мусора Go не принимает каждое машинное слово в стеке за указатель?
Сборщик мусора Go использует метаданные, сгенерированные компилятором: карты стека и сведения о времени жизни локальных переменных. В безопасных точках выполнения они показывают, какие позиции стека и регистры содержат указатели, поэтому GC сканирует только потенциальные ссылки, а обычные числа и байтовые данные игнорирует.
Точное сканирование нужно для того, чтобы сборщик мусора мог работать с обычной типизированной памятью без консервативного предположения, что любое значение, похожее на адрес, является указателем. Такой подход уменьшает число ложных ссылок и позволяет освобождать больше недостижимых объектов.
В Go сборка мусора выполняется конкурентно с пользовательским кодом. Поэтому GC должен уметь быстро и корректно определить корни не только в куче, но и в стеках горутин, включая их состояние в безопасных точках.
Стек содержит разнородные данные: указатели, целые числа, длины срезов, флаги, части структур и временные значения. Если считать каждое машинное слово указателем, случайное число может удерживать объект в памяти или заставить GC просканировать область, которая указателем не является.
Обратная ошибка опаснее: если не заметить настоящий указатель, живой объект может быть ошибочно признан недостижимым. Поэтому информация о расположении указателей должна формироваться компилятором, а не восстанавливаться по внешнему виду битового значения.
Для функций компилятор формирует stack maps — карты расположения указателей в кадрах стека. Эти карты связаны с конкретными безопасными точками и описывают, какие слоты содержат указатели, а какие являются непросматриваемыми данными.
Кроме расположения указателей, компилятор учитывает liveness — актуальность переменных. Локальная переменная может уже не использоваться, даже если её машинное значение физически осталось в стеке. После завершения её времени жизни соответствующий слот не должен удерживать объект как корень.
При остановке горутины в безопасной точке GC получает описание её стека и сканирует только отмеченные указательные позиции. Для регистров также существуют необходимые сведения, когда они могут содержать живые указатели. Объекты в куче дополнительно описываются информацией о типе: GC знает, какие поля объекта являются указателями.
Это не означает, что любой указатель можно скрыть в произвольном числе. Значение типа uintptr не считается ссылкой GC и не удерживает объект от сборки. Для хранения ссылки, которую должен видеть сборщик, используется указательный тип; переходы через unsafe требуют соблюдения правил времени жизни и могут нарушить корректность программы.
Карты стека также помогают безопасно перемещать или расширять стек горутины: runtime знает, какие значения нужно обновить как указатели. При этом сам Go GC является неперемещающим сборщиком объектов кучи, поэтому данная возможность относится прежде всего к обработке стеков, а не к компактированию всей кучи.
Основной компромисс состоит в дополнительной работе компилятора и runtime с метаданными. Однако консервативное сканирование обычно создало бы больше ложных удержаний и лишних проверок, тогда как точные карты делают обработку корней предсказуемее и эффективнее.
В сервисе есть обработчик, который временно работает с большими структурами: одна содержит в основном байтовые буферы, другая — множество ссылочных полей. Разработчик видит, что обе структуры находятся на стеке, и ожидает одинаковую стоимость их обработки GC.
Вариант «ничего не менять» сохраняет простую реализацию, но структура с большим числом указателей требует проверки большего числа слотов и ссылок. Попытка спрятать ссылки в uintptr может уменьшить видимую для GC область, но создаёт риск преждевременного освобождения объектов и некорректного доступа.
Корректное решение — оставить ссылки типизированными, уменьшить время жизни крупных временных значений и разделить данные так, чтобы указательные поля не смешивались без необходимости с большими буферами. Это сохраняет точность GC и позволяет компилятору исключать из сканирования неп указательные области; конкретный выигрыш нужно проверять профилированием, а не предполагать только по размеру структуры.
Нет. Важен не внешний вид битов, а информация компилятора о типе и живости значения. Обычное целое число или uintptr не становится корнем только потому, что его числовое значение совпадает с адресом объекта.
Компилятор анализирует фактическое использование переменной. После последнего значимого обращения её слот может быть исключён из набора живых корней, даже если функция ещё продолжается. Поэтому для специальных случаев, когда необходимо гарантировать жизнь объекта до определённого места, применяют предусмотренные runtime-механизмы, а не рассчитывают на наличие старого значения в стеке.
Нет. Для стека используются карты конкретного кадра и точки выполнения, а для объекта в куче — сведения о его типе или форме размещённых указателей. В обоих случаях сборщик ищет только известные указательные поля, но источник этой информации различается: состояние функции для стека и описание объекта для кучи.