Go 1.26: как сборщик мусора Green Tea работает с кучей

Go 1.25 (вышел в 2025 году) представил экспериментальный сборщик мусора Green Tea, а в Go 1.26, выпущенном несколько месяцев назад, он стал сборщиком по умолчанию. Автор блога TheConsensus.dev написал небольшую программу-визуализатор кучи (heapwalk.go), чтобы показать, как это работает на практике, и сравнил поведение Go с C#/.NET.
В первом эксперименте программа случайным образом создаёт 100 объектов трёх размеров, 32, 64 и 128 байт (Small/Medium/Large), и печатает карту адресов кучи до и после явного вызова сборщика мусора. Результат: несмотря на случайный порядок выделения, Go группирует объекты одного размера рядом друг с другом, это следствие size-class-аллокатора, который у Go унаследован от того же подхода, что и в tcmalloc: объекты выравниваются по ближайшему размерному классу и кладутся в общий смежный блок памяти («span» в терминологии Go) из одной или нескольких страниц по 8 КиБ. После запуска runtime.GC() расположение объектов не меняется вовсе, Go никогда не перемещает объекты в памяти (non-moving collector). В аналогичном тесте на C# (HeapWalk.cs.NET 10 SDK) объекты разных размеров идут вперемешку без группировки; автор также отмечает, что в других сценариях сборщик мусора .NET реально двигает объекты по памяти (компактирующий сборщик), в отличие от Go.
Далее автор разбирает фазу разметки (mark) в модели «mark and sweep»: сборщик стартует от корней (глобальные переменные, локальные переменные на стеке) и обходит граф достижимых объектов по указателям; всё, что не было помечено, на фазе sweep освобождается как мёртвое. Проблема в том, что если объект A ссылается на объекты B/C/D разных размеров или созданные в разное время, они физически лежат в разных участках памяти, и обход по указателям превращается в случайные обращения к памяти, это плохо для кэша процессора. В Green Tea логика меняется: вместо того чтобы идти по каждому указателю по одному, сборщик сканирует span памяти целиком, находит в нём указатели и ставит в очередь на сканирование те span, куда эти указатели ведут. Автор прямо оговаривает, что напрямую визуализировать сам путь разметки без патчей исходников Go не удалось, но с помощью профилировщика perf показывает: у Green Tea меньше промахов кэша на тысячу инструкций и выше скорость выполнения программ по сравнению со старым сборщиком.
Для второго эксперимента используется структура Node с четырьмя указателями (a, b, c, d) на 2 миллиона узлов в двух режимах: «packed», каждый узел ссылается на несколько следующих по порядку в массиве (кэш-дружественное расположение), и «scattered», ссылки расставлены случайно (кэш-недружественное расположение). Именно на этом сценарии проявляется оговорённая в начале статьи проблема: неперемещающий сборщик Go, в отличие от компактирующих сборщиков вроде .NET, не может переупаковать разреженные страницы памяти, страницу с горсткой ещё живых объектов среди уже мёртвых нельзя целиком освободить и переиспользовать, пока живы хотя бы отдельные объекты на ней.
Ключевые факты
- Go 1.25 (2025) представил экспериментальный сборщик мусора Green Tea; в Go 1.26, вышедшем несколько месяцев назад, он стал сборщиком по умолчанию
- Эксперимент со 100 объектами трёх размеров (32/64/128 байт) показывает: Go группирует объекты одного размера рядом в памяти благодаря size-class-аллокатору (родственному tcmalloc) и никогда не перемещает объекты после сборки мусора (non-moving collector)
- В аналогичном тесте на C#/.NET объекты разных размеров идут вперемешку, а в других сценариях сборщик .NET реально двигает объекты в памяти, это ключевое отличие от Go
- Green Tea меняет фазу разметки: вместо обхода указателей по одному сборщик сканирует span памяти целиком и на основе найденных указателей ставит в очередь следующие span, по данным
perf, это даёт меньше промахов кэша на тысячу инструкций и более быстрое выполнение программ - На тесте с 2 млн узлов (по 4 указателя на узел) в «packed»- и «scattered»-раскладке проявляется нерешённая проблема: неперемещающий сборщик Go не может переупаковать разреженные страницы памяти с горсткой живых объектов среди мёртвых
Почему это важно
Переход Green Tea в статус сборщика по умолчанию в Go 1.26 меняет поведение сборки мусора у всех Go-программ без единой правки прикладного кода. Автор не пересказывает официальный анонс, а на своём коде и данных профилировщика perf буквально показывает, как рантайм раскладывает объекты по памяти и почему новая схема сканирования кучи меньше нагружает кэш процессора, редкий случай, когда внутреннюю механику сборщика мусора можно увидеть напрямую, а не поверить на слово.
Кому это важно
Материал адресован Go-разработчикам и инженерам по производительности бэкендов, которые настраивают память приложений под нагрузку, а также тем, кто сравнивает управление памятью в разных средах выполнения: статья наглядно противопоставляет неперемещающий (non-moving) сборщик Go компактирующему сборщику .NET на одинаковом эксперименте.
Как это применить
Автор публикует полный воспроизводимый код обеих программ (heapwalk.go для Go и HeapWalk.cs для C#) вместе с командами установки Go 1.26 и .NET 10 SDK, так что эксперимент можно повторить у себя и профилировать собственные программы через perf, сравнивая промахи кэша до и после Green Tea; в Go 1.26 он включён по умолчанию, отдельно ничего настраивать не требуется.
Можно ли доверять
Это практический разбор с полным исходным кодом, точными командами воспроизведения и ссылкой на измерения через perf, а не пересказ маркетингового анонса; методология прозрачна и проверяема. Автор честно оговаривает предел собственного метода: показать сам путь разметки без патчей исходников Go не удалось, вывод о меньшем числе промахов кэша опирается на косвенные измерения профилировщика, а не на прямую визуализацию сканирования указателей.
Риски и подводные камни
Green Tea ускоряет сканирование кучи, но не снимает системное ограничение non-moving коллектора Go: раз объекты в памяти не двигаются, страница с горсткой ещё живых объектов среди уже мёртвых не может быть освобождена и переиспользована целиком, это и есть та самая «проблема разреженных страниц», которую автор заявляет во вступлении статьи и подтверждает на тесте со «scattered»-раскладкой 2 миллионов узлов.
«Хотя мы выделяли память случайным образом вперемешку для объектов разных размеров, видно, что рантайм Go расставляет объекты одного размера рядом друг с другом. И даже после запуска сборщика мусора ничего не сдвинулось с места.»
— TheConsensus.dev, разбор сборщика мусора Go 1.26