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

Что такое сборщик мусора?
GC в Go — это как уборщик, который автоматически выкидывает мусор (ненужные объекты) из памяти, чтобы программа не тратила лишнее место. Без него пришлось бы вручную освобождать память, как в C++, а это сложно и чревато ошибками.
Зачем нужен?
-
Освобождает память от объектов, на которые никто не ссылается.
-
Делает программу быстрой, минимизируя паузы.
-
Балансирует, сколько памяти и процессора использовать.
Go выделяет память в куче (место для объектов, которые живут долго). GC следит, чтобы куча не разрасталась, убирая "мусор".
Как работает GC в Go?
GC в Go использует трехцветный алгоритм, который работает одновременно с программой (конкурентно) и по частям (инкрементально). Давайте разберём, как это устроено, с помощью гофера.
Трехцветный алгоритм
Представьте, что память — это куча коробок (объектов). GC раскрашивает их в три цвета:
-
Белые: Коробки, которые ещё не проверены. Возможно, это мусор.
-
Серые: Коробки, которые нужны, но их содержимое (указатели) ещё не проверено.
-
Чёрные: Коробки, которые точно нужны и проверены.
Шаги:
-
Все коробки изначально белые.
-
GC начинает с "корней" (например, глобальные переменные или стек) — они становятся чёрными, а их указатели — серыми.
-
GC берёт серую коробку, проверяет её указатели на другие объекты, делает эту коробку чёрной, а объекты, на которые она ссылается, — серыми.
-
Когда серых коробок не остаётся, белые — это мусор, и их убирают.

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

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

Барьер записи (write barrier)
Барьер записи — это как охранник, который следит, чтобы новые объекты не ускользнули от GC. Когда программа добавляет новые указатели (например, связывает объект A с объектом B), барьер отмечает их как серые, чтобы GC их проверил.
-
Проблема: Без барьера GC может случайно удалить нужный объект.
-
Аналогия: Гофер-охранник ставит печать на новых коробках, чтобы уборщик их заметил.

Как настроить GC?
Go даёт несколько рычагов, чтобы управлять GC:
-
GOGC: Число (по умолчанию 100), которое решает, как часто запускать GC. Значение 100 означает, что GC запускается, когда размер кучи удваивается (живые объекты + мусор). Если GOGC=200, GC реже работает, но памяти нужно больше. Если GOGC=50, GC работает чаще, но памяти меньше.
-
GOMEMLIMIT: (с Go 1.19) ограничивает, сколько памяти можно использовать. Если лимит близко, GC работает чаще.
-
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.

Автор: nick152
