golang

Разбираемся в сборщике мусора Go: просто и с гофером

  • вторник, 1 июля 2025 г. в 00:00:08
https://habr.com/ru/articles/923398/

Я решил написать эту статью в первую очередь для себя, потому что перечитал кучу материалов про сборщик мусора (GC) в Go, и почти все они были слишком сложными. Моя цель — объяснить, как работает GC, что такое инкрементальность и барьер записи, так, чтобы я сам понял и запомнил и, возможно, стал полезным для других. А чтобы было веселее, я добавил гофера — маскота Go — в забавных иллюстрациях, которые помогут визуализировать идеи. Если вы, как и я, хотите разобраться в GC без лишней головной боли, эта статья для вас!

Что такое сборщик мусора?

GC в Go — это как уборщик, который автоматически выкидывает мусор (ненужные объекты) из памяти, чтобы программа не тратила лишнее место. Без него пришлось бы вручную освобождать память, как в C++, а это сложно и чревато ошибками.

Зачем нужен?

  • Освобождает память от объектов, на которые никто не ссылается.

  • Делает программу быстрой, минимизируя паузы.

  • Балансирует, сколько памяти и процессора использовать.

Go выделяет память в куче (место для объектов, которые живут долго). GC следит, чтобы куча не разрасталась, убирая "мусор".

Как работает GC в Go?

GC в Go использует трехцветный алгоритм, который работает одновременно с программой (конкурентно) и по частям (инкрементально). Давайте разберём, как это устроено, с помощью гофера.

Трехцветный алгоритм

Представьте, что память — это куча коробок (объектов). GC раскрашивает их в три цвета:

  • Белые: Коробки, которые ещё не проверены. Возможно, это мусор.

  • Серые: Коробки, которые нужны, но их содержимое (указатели) ещё не проверено.

  • Чёрные: Коробки, которые точно нужны и проверены.

Шаги:

  1. Все коробки изначально белые.

  2. GC начинает с "корней" (например, глобальные переменные или стек) — они становятся чёрными, а их указатели — серыми.

  3. GC берёт серую коробку, проверяет её указатели на другие объекты, делает эту коробку чёрной, а объекты, на которые она ссылается, — серыми.

  4. Когда серых коробок не остаётся, белые — это мусор, и их убирают.

Конкурентность

GC в Go работает одновременно с программой, чтобы не тормозить её. Это называется конкурентность. Но есть два момента, когда программа ненадолго останавливается (это Stop The World, STW):

  • В начале, чтобы отметить корни (доли миллисекунд).

  • В конце, чтобы завершить проверку.

Остальное время GC работает в фоновом режиме, как гофер, который убирает мусор, пока вы продолжаете работать.

Инкрементальность

Инкрементальный GC — это когда уборка идёт по чуть-чуть, а не всё сразу. Это как убирать комнату по одному углу за раз, чтобы не прерывать вечеринку.

  • Зачем? Чтобы программа не "зависала" надолго.

  • Как работает? GC помечает несколько объектов, потом даёт программе поработать, затем продолжает.

Барьер записи (write barrier)

Барьер записи — это как охранник, который следит, чтобы новые объекты не ускользнули от GC. Когда программа добавляет новые указатели (например, связывает объект A с объектом B), барьер отмечает их как серые, чтобы GC их проверил.

  • Проблема: Без барьера GC может случайно удалить нужный объект.

  • Аналогия: Гофер-охранник ставит печать на новых коробках, чтобы уборщик их заметил.

Как настроить GC?

Go даёт несколько рычагов, чтобы управлять GC:

  1. GOGC: Число (по умолчанию 100), которое решает, как часто запускать GC. Значение 100 означает, что GC запускается, когда размер кучи удваивается (живые объекты + мусор). Если GOGC=200, GC реже работает, но памяти нужно больше. Если GOGC=50, GC работает чаще, но памяти меньше.

  2. GOMEMLIMIT: (с Go 1.19) ограничивает, сколько памяти можно использовать. Если лимит близко, GC работает чаще.

  3. GOMAXPROCS: Сколько процессоров использовать для программы и GC.

Как оптимизировать память?

1. Выделение на стеке

Компилятор Go использует анализ побега (escape analysis), чтобы решить, где выделять память: на стеке (быстро, без GC) или в куче (для GC). Если переменная "убегает" в кучу (например, возвращается как указатель), это нагружает GC.

func bad() *int {
    x := 42
    return &x // Убегает в кучу
}
func good() int {
    x := 42
    return x // На стеке
}

2. Пул объектов (sync.Pool)

Пул позволяет использовать объекты повторно, а не создавать новые:

var pool = sync.Pool{
    New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
    buf := pool.Get().(*bytes.Buffer)
    defer pool.Put(buf)
    buf.Reset()
    // Работа с buf
}

3. Меньше аллокаций

  • Используйте strings.Builder для строк вместо конкатенации.

  • Избегайте интерфейсов, если они не нужны, — они создают лишние объекты.

4. Профилирование

  • Используйте pprof:

    go tool pprof -http=:8080 mem.prof
  • Отслеживайте GC с GODEBUG=gctrace=1.

5. Настройка GOGC и GOMEMLIMIT

  • Увеличьте GOGC для высоконагруженных систем.

  • Используйте GOMEMLIMIT для контейнеров.

Заключение

GC в Go — мощный инструмент, который я теперь понимаю! Надеюсь, что гофер помог и вам с легкостью разобраться в этой непростой теме.

Ключевые моменты:

  • Трехцветный алгоритм: Белый, серый, чёрный.

  • Конкурентность и инкрементальность: Минимизируют STW.

  • Барьер записи: Защищает объекты.

  • Оптимизации: Escape analysis, sync.Pool, настройка GOGC.