Введение в эволюцию управления памятью в Go

Когда размер кучи вашего бэкенда переваливает за сотни гигабайт, стандартный сборщик мусора начинает незаметно отъедать драгоценные миллисекунды CPU на каждый запрос (почти как Chrome, открывающий десятую вкладку). Для инженеров, выжимающих максимум из инфраструктуры, это всегда компромисс между задержками и утилизацией памяти. И пока индустрия гадала, какой ответ рантайм Go даст на вызовы современных enterprise-нагрузок, разработчики выкатили решение, способное перевернуть привычные представления о производительности.

Язык программирования Go всегда славился своей простотой, высокой производительностью и эффективной моделью параллелизма. Однако одной из самых обсуждаемых частей рантайма Go исторически оставался сборщик мусора (Garbage Collector, GC). Начиная с ранних версий, Go использовал параллельный неперемещающий (non-moving) сборщик мусора на основе алгоритма Tri-color mark-sweep. Этот подход избавил разработчиков от необходимости вручную управлять памятью (и больше не нужно молиться утечкам в деструкторах), обеспечивая при этом минимальные паузы stop-the-world (STW), которые исчисляются микросекундами.

Тем не менее, мир высоконагруженных систем не стоит на месте. Объемы оперативной памяти серверов растут, размеры кучи (heap) исчисляются сотнями гигабайт, а требования к задержкам (latency) и пропускной способности (throughput) становятся все жестче. Традиционный GC в Go, несмотря на все оптимизации прошлых лет, упирался в фундаментальные архитектурные ограничения, такие как неэффективность работы с кэшем процессора и проблема фрагментации памяти.

Ситуация кардинально изменилась с выходом релизов Go 1.25 и Go 1.26. В версии Go 1.25 разработчики языка представили экспериментальный сборщик мусора под интригующим названием Green Tea GC, который активировался специальным флагом сборки GOEXPERIMENT=greenteagc. Успех экспериментальных прогонов превзошел ожидания команды компилятора, и уже в релизе Go 1.26 Green Tea GC стал сборщиком мусора по умолчанию для всех приложений. В этой статье мы подробно разберем, как устроен этот алгоритм, за счет чего достигается экономия процессорного времени и как он меняет поведение рантайма при сканировании кучи.

Анатомия Green Tea GC: что изменилось под капотом

Чтобы понять суть изменений, давайте заглянем под капот и проследим, как эволюционировал обход памяти, из-за которого процессоры годами простаивали в ожидании данных из оперативной памяти.

Чтобы понять суть изменений, нужно вспомнить, как классический сборщик мусора Go обходит кучу. Традиционный GC строит граф объектов, начиная с корневых элементов (goroutine stacks, глобальные переменные, регистры), и рекурсивно помечает достижимые объекты. При больших объемах памяти и миллиардах мелких аллокаций этот процесс приводит к интенсивному чтению метаданных и самих объектов, что пагубно влияет на кэш процессора (CPU cache misses).

Green Tea GC переосмысливает подход к обходу памяти и организации структур данных рантайма. Главная цель нового алгоритма — сделать процесс сборки мусора более «кэш-дружелюбным» (cache-friendly). По сути, Green Tea оптимизирует пространственную и временную локальность данных, к которым обращается рантайм во время фазы сканирования и очистки.

Практический пример: нагрузка на аллокатор

Представьте, что вы проектируете сервис реального времени для процессинга финансовых транзакций (или просто пишите очередной микросервис, который внезапно взлетел на Product Hunt), где миллионы связанных объектов создаются и уничтожаются каждую секунду, превращая кучу в минное поле для старого сборщика мусора.

Рассмотрим базовый пример кода на Go, который создает плотные структуры данных, активно нагружающие аллокатор:

package main

import (
	"fmt"
	"runtime"
	"time"
)

type Node struct {
	Value int64
	Left  *Node
	Right *Node
}

func buildTree(depth int) *Node {
	if depth == 0 {
		return nil
	}
	return &Node{
		Value: int64(depth),
		Left:  buildTree(depth - 1),
		Right: buildTree(depth - 1),
	}
}

func main() {
	var m runtime.MemStats