Разбираемся в сборщике мусора Go: просто и с гофером
- вторник, 1 июля 2025 г. в 00:00:08
Я решил написать эту статью в первую очередь для себя, потому что перечитал кучу материалов про сборщик мусора (GC) в Go, и почти все они были слишком сложными. Моя цель — объяснить, как работает GC, что такое инкрементальность и барьер записи, так, чтобы я сам понял и запомнил и, возможно, стал полезным для других. А чтобы было веселее, я добавил гофера — маскота Go — в забавных иллюстрациях, которые помогут визуализировать идеи. Если вы, как и я, хотите разобраться в GC без лишней головной боли, эта статья для вас!
GC в Go — это как уборщик, который автоматически выкидывает мусор (ненужные объекты) из памяти, чтобы программа не тратила лишнее место. Без него пришлось бы вручную освобождать память, как в C++, а это сложно и чревато ошибками.
Зачем нужен?
Освобождает память от объектов, на которые никто не ссылается.
Делает программу быстрой, минимизируя паузы.
Балансирует, сколько памяти и процессора использовать.
Go выделяет память в куче (место для объектов, которые живут долго). GC следит, чтобы куча не разрасталась, убирая "мусор".
GC в Go использует трехцветный алгоритм, который работает одновременно с программой (конкурентно) и по частям (инкрементально). Давайте разберём, как это устроено, с помощью гофера.
Представьте, что память — это куча коробок (объектов). GC раскрашивает их в три цвета:
Белые: Коробки, которые ещё не проверены. Возможно, это мусор.
Серые: Коробки, которые нужны, но их содержимое (указатели) ещё не проверено.
Чёрные: Коробки, которые точно нужны и проверены.
Шаги:
Все коробки изначально белые.
GC начинает с "корней" (например, глобальные переменные или стек) — они становятся чёрными, а их указатели — серыми.
GC берёт серую коробку, проверяет её указатели на другие объекты, делает эту коробку чёрной, а объекты, на которые она ссылается, — серыми.
Когда серых коробок не остаётся, белые — это мусор, и их убирают.
GC в Go работает одновременно с программой, чтобы не тормозить её. Это называется конкурентность. Но есть два момента, когда программа ненадолго останавливается (это Stop The World, STW):
В начале, чтобы отметить корни (доли миллисекунд).
В конце, чтобы завершить проверку.
Остальное время GC работает в фоновом режиме, как гофер, который убирает мусор, пока вы продолжаете работать.
Инкрементальный GC — это когда уборка идёт по чуть-чуть, а не всё сразу. Это как убирать комнату по одному углу за раз, чтобы не прерывать вечеринку.
Зачем? Чтобы программа не "зависала" надолго.
Как работает? GC помечает несколько объектов, потом даёт программе поработать, затем продолжает.
Барьер записи — это как охранник, который следит, чтобы новые объекты не ускользнули от GC. Когда программа добавляет новые указатели (например, связывает объект A с объектом B), барьер отмечает их как серые, чтобы GC их проверил.
Проблема: Без барьера GC может случайно удалить нужный объект.
Аналогия: Гофер-охранник ставит печать на новых коробках, чтобы уборщик их заметил.
Go даёт несколько рычагов, чтобы управлять GC:
GOGC: Число (по умолчанию 100), которое решает, как часто запускать GC. Значение 100 означает, что GC запускается, когда размер кучи удваивается (живые объекты + мусор). Если GOGC=200, GC реже работает, но памяти нужно больше. Если GOGC=50, GC работает чаще, но памяти меньше.
GOMEMLIMIT: (с Go 1.19) ограничивает, сколько памяти можно использовать. Если лимит близко, GC работает чаще.
GOMAXPROCS: Сколько процессоров использовать для программы и GC.
Компилятор Go использует анализ побега (escape analysis), чтобы решить, где выделять память: на стеке (быстро, без GC) или в куче (для GC). Если переменная "убегает" в кучу (например, возвращается как указатель), это нагружает GC.
func bad() *int {
x := 42
return &x // Убегает в кучу
}
func good() int {
x := 42
return x // На стеке
}
Пул позволяет использовать объекты повторно, а не создавать новые:
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
}
Используйте strings.Builder для строк вместо конкатенации.
Избегайте интерфейсов, если они не нужны, — они создают лишние объекты.
Используйте pprof:
go tool pprof -http=:8080 mem.prof
Отслеживайте GC с GODEBUG=gctrace=1.
Увеличьте GOGC для высоконагруженных систем.
Используйте GOMEMLIMIT для контейнеров.
GC в Go — мощный инструмент, который я теперь понимаю! Надеюсь, что гофер помог и вам с легкостью разобраться в этой непростой теме.
Ключевые моменты:
Трехцветный алгоритм: Белый, серый, чёрный.
Конкурентность и инкрементальность: Минимизируют STW.
Барьер записи: Защищает объекты.
Оптимизации: Escape analysis, sync.Pool, настройка GOGC.