Память и сборщик мусора в Go: как GC управляет ресурсами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Память и сборщик мусора в Go: как не сойти с ума от GC

Вы когда-нибудь задумывались, куда уходит память в вашем Go-приложении? Особенно когда оно начинает жрать гигабайты, а вы всего лишь парсите JSON? Знакомо? Добро пожаловать в мир сборщика мусора (GC) в Go — таинственного механизма, который либо спасает ваш проект, либо превращает его в тормозную платформу. Сегодня мы разберем, как работает память в Go, как GC решает, что пора убирать, и как не попасть в ловушку "ожидания STW".

Если вы пишете на Go хотя бы пару месяцев, вы уже сталкивались с этим: внезапные паузы, рост потребления памяти, загадочные gc pause в профилях. Давайте заглянем под капот и разберемся, как управлять памятью в Go осознанно.

Как устроена память в Go: куча и стек

Прежде чем говорить о сборщике мусора, давайте поймем, где вообще живут данные в Go. В отличие от языков вроде C, где вы сами отвечаете за malloc/free, Go использует автоматическое управление памятью. Но это не значит, что всё валится в одну кучу.

В Go есть два основных региона памяти:

  • Стек (stack) — быстрая, автоматически управляемая память для локальных переменных. Каждая горутина имеет свой стек (начинается с 2 КБ и динамически растет).
  • Куча (heap) — медленная, но гибкая память для данных, которые "переживают" вызов функции.

Главный вопрос: что попадает в кучу, а что остается на стеке? За это отвечает escape analysis — умный механизм компилятора Go. Он анализирует, "убегает" ли переменная за пределы функции. Если да — она едет в кучу. Если нет — остается на стеке, и GC о ней даже не узнает.

Пример:

package main

func createPointer() *int {
    x := 42
    return &x // x "убегает" — попадает в кучу
}

func noEscape() int {
    x := 42
    return x // x остается на стеке
}

В первом случае компилятор видит, что указатель на x возвращается наружу, и размещает переменную в куче. Во втором — всё просто и быстро. Это и есть управление памятью в Go на микроуровне.

Сборщик мусора в Go: как он работает

Теперь о главном герое — сборщике мусора (GC). В Go используется параллельный, триколорный, неинкрементный сборщик с алгоритмом "mark-and-sweep". Звучит страшно? На самом деле всё логично.

GC в Go проходит три фазы:

  1. Mark Setup (STW) — короткая пауза для включения write barrier. Обычно занимает 10-30 микросекунд.
  2. Marking (Concurrent) — основная работа: GC обходит граф объектов, помечая живые. Выполняется параллельно с вашим кодом.
  3. Sweep (Concurrent + STW) — очистка мертвых объектов. Частично делается параллельно, но финальная синхронизация может вызвать короткую паузу.

Ключевая особенность GC в Go — он не делает дефрагментацию. Это значит, что после сборки память может фрагментироваться, но Go использует битовые карты для отслеживания свободных блоков, что делает выделение памяти быстрым.

Пример работы GC можно увидеть в логах:

GODEBUG=gctrace=1 go run main.go

gc 1 @0.013s 4%: 0.031+0.42+0.017 ms clock, 0.25+0.10/0.42/0.0+0.13 ms cpu, 4->4->2 MB, 5 MB goal, 8 P

Здесь: 0.031+0.42+0.017 ms — это STW + Mark + STW. 4->4->2 MB — размер кучи до, после mark и после sweep.

Почему GC тормозит: STW и паузы

Главный враг производительности — STW (Stop The World). Хотя Go старается минимизировать паузы, они всё равно случаются. Особенно если:

  • У вас много указателей (GC должен обойти все объекты)
  • Куча большая (больше объектов — дольше marking)
  • Вы создаете много мусора (high allocation rate)

В Go 1.19+ паузы STW обычно не превышают 100-500 микросекунд, но если у вас гигабайты данных, может быть и больше. Как проверить? Используйте pprof или trace:

import (
    "runtime/pprof"
    "os"
)

func main() {
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    // ваш код
}

Анализируйте: если видите много времени в runtime.gcAssistAlloc или runtime.mallocgc — ваш GC под нагрузкой.

Настройка GC: GOGC и память

Сборщик мусора в Go настраивается через переменную окружения GOGC. Она определяет, насколько сильно может вырасти куча перед следующей сборкой. По умолчанию GOGC=100 — это значит, что GC запустится, когда размер кучи увеличится на 100% относительно предыдущего размера после сборки.

Примеры настройки:

  • GOGC=200 — реже сборки, но больше пиковое потребление памяти
  • GOGC=50 — чаще сборки, меньше память, но больше CPU
  • GOGC=off — отключить GC (только для тестов!)

Как выбрать? Если ваше приложение чувствительно к задержкам (например, API), лучше уменьшить GOGC (50-75). Если память критична, а CPU не жалко — оставить 100 или увеличить до 150.

Но есть нюанс: управление памятью в Go не ограничивается GOGC. Вы можете влиять на аллокации прямо в коде.

Практические советы: как уменьшить нагрузку на GC

Хватит теории. Вот 5 конкретных приемов, которые помогут вашему приложению меньше грузить сборщик мусора:

  1. Используйте sync.Pool — переиспользуйте объекты вместо создания новых.
  2. Избегайте указателей в горячих путях — передавайте структуры по значению, если они маленькие.
  3. Предпочитайте массивы и слайсы с фиксированной длиной — меньше реаллокаций.
  4. Не смешивайте маленькие и большие объекты —

Блоги

Рекомендаций по книгам