Перейти к основному содержимому

Сборка мусора (Garbage Collection)

Введение

Представьте, что вы арендуете квартиру. Когда вы въезжаете, вы приносите свои вещи. Когда что-то становится ненужным, вы это выбрасываете. Если бы вы никогда ничего не выбрасывали, квартира быстро превратилась бы в свалку.

Примерно так же работает память в программах. Когда программа создает переменные, объекты или структуры данных, она "арендует" память. Garbage Collector (GC) — это тот, кто автоматически "выбрасывает мусор", освобождая память от объектов, которые программе больше не нужны.

Зачем нужен GC?

В языках без автоматической сборки мусора (например, C или C++) программист должен вручную освобождать память. Это приводит к двум типичным проблемам:

Утечки памяти — забыли освободить память, и она "висит" до конца работы программы, постепенно заполняя всю доступную память.

Висячие указатели — освободили память слишком рано, а потом попытались к ней обратиться. Программа падает или ведет себя непредсказуемо.

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

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

Основной принцип: достижимость

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

Корневые точки — это:

  • Глобальные переменные
  • Локальные переменные в стеке активных функций
  • Регистры процессора

Пример:

func example() {
x := &SomeStruct{} // x достижим — он в стеке функции
doSomething(x)
// После выхода из функции x становится недостижимым
// и может быть удален сборщиком мусора
}

Три фазы работы GC

GC в Go работает в три этапа:

1. Mark Setup (подготовка к маркировке) — GC приостанавливает работу программы на очень короткое время (обычно доли миллисекунды), чтобы подготовиться к сканированию памяти.

2. Marking (маркировка) — GC проходит по всем достижимым объектам и помечает их как "живые". Это происходит параллельно с работой программы. GC использует специальные механизмы (write barriers), чтобы отслеживать изменения в памяти во время этого процесса.

3. Sweep (очистка) — GC удаляет все объекты, которые не были помечены как "живые". Это тоже происходит параллельно с программой.

Tricolor Algorithm (трехцветный алгоритм)

GC в Go использует трехцветный алгоритм маркировки. Представьте, что каждый объект можно покрасить в один из трех цветов:

  • Белый — объект еще не проверен
  • Серый — объект проверен, но его "соседи" (объекты, на которые он ссылается) еще нет
  • Черный — объект и все его "соседи" проверены

Процесс:

  1. Вначале все объекты белые
  2. Корневые объекты становятся серыми
  3. GC берет серый объект, проверяет все объекты, на которые он ссылается (они становятся серыми), а сам объект делает черным
  4. Повторяет шаг 3, пока не закончатся серые объекты
  5. Все оставшиеся белые объекты — это мусор, их можно удалить

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

GOGC

Основной параметр настройки GC — это GOGC. Он определяет, насколько должна вырасти куча (heap), прежде чем запустится следующий цикл GC.

Значение по умолчанию — 100, что означает: "запустить GC, когда размер кучи вырастет на 100% от размера живых объектов после последнего GC".

# Запустить с GOGC=200 (GC реже, но больше памяти)
GOGC=200 ./myapp

# Отключить GC (не рекомендуется!)
GOGC=off ./myapp

Пример из кода:

import "runtime/debug"

func main() {
// Установить GOGC = 200
debug.SetGCPercent(200)

// Вернуть значение по умолчанию
debug.SetGCPercent(100)
}

GOMEMLIMIT (Go 1.19+)

Новый параметр, который устанавливает мягкий лимит памяти:

# Ограничить использование памяти 1 ГБ
GOMEMLIMIT=1GiB ./myapp

Из кода:

import "runtime/debug"

func main() {
// Установить лимит памяти в 1 ГБ
debug.SetMemoryLimit(1 << 30) // 1 * 1024 * 1024 * 1024
}

Практические советы

Когда запускать GC вручную?

Обычно не стоит. Но есть исключения:

import "runtime"

func processLargeFile() {
data := loadHugeData() // Загрузили много данных
processData(data)
data = nil // Явно показываем, что data больше не нужен

// Если знаем, что после этого будет долгая операция без аллокаций,
// можно запустить GC, чтобы освободить память прямо сейчас
runtime.GC()

doSomethingElseForLongTime()
}

Как уменьшить нагрузку на GC?

1. Используйте sync.Pool для переиспользования объектов

var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}

func process() {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()

// Используем buf...
}

2. Предвыделяйте слайсы нужного размера

// Плохо: каждое append может вызвать аллокацию
items := []Item{}
for i := 0; i < 1000; i++ {
items = append(items, Item{})
}

// Хорошо: одна аллокация
items := make([]Item, 0, 1000)
for i := 0; i < 1000; i++ {
items = append(items, Item{})
}

3. Избегайте создания множества мелких объектов

// Плохо
type Point struct {
X, Y *float64 // Каждое поле — отдельный объект в куче
}

// Хорошо
type Point struct {
X, Y float64 // Значения, а не указатели
}

Мониторинг GC

Включите статистику GC:

GODEBUG=gctrace=1 ./myapp

Вывод покажет информацию о каждом цикле GC:

gc 1 @0.003s 0%: 0.015+0.59+0.096 ms clock, 0.18+0.35/1.0/3.0+1.1 ms cpu, 4->4->3 MB, 5 MB goal, 12 P

Ключевые метрики:

  • 4->4->3 MB — размер кучи до GC, после маркировки, после очистки
  • 5 MB goal — целевой размер кучи
  • 0.59 ms — время маркировки

Из кода можно получить статистику так:

import "runtime"

var m runtime.MemStats
runtime.ReadMemStats(&m)

fmt.Printf("Allocated: %d MB\n", m.Alloc / 1024 / 1024)
fmt.Printf("Total Allocated: %d MB\n", m.TotalAlloc / 1024 / 1024)
fmt.Printf("Num GC: %d\n", m.NumGC)
fmt.Printf("Pause Total: %v\n", m.PauseTotalNs)

Типичные проблемы и их решение

Проблема 1: Слишком частый GC

Симптом: программа работает медленно, в логах много циклов GC

Решение: увеличьте GOGC или установите GOMEMLIMIT

Проблема 2: Программа использует слишком много памяти

Симптом: RSS процесса постоянно растет

Решение:

  • Проверьте, нет ли утечек (goroutine, которые держат ссылки на большие объекты)
  • Используйте pprof для поиска источников аллокаций
  • Уменьшите GOGC или установите GOMEMLIMIT

Проблема 3: Долгие паузы GC

Симптом: периодические задержки в работе программы

Решение:

  • В современных версиях Go паузы обычно меньше 1 мс
  • Если паузы длинные, проверьте, нет ли огромных объектов с множеством указателей
  • Рассмотрите использование структур без указателей там, где возможно

Заключение

Сборщик мусора в Go — это эффективный инструмент, который работает параллельно с вашей программой и требует минимального вмешательства. Основные принципы:

  • GC автоматически находит и удаляет недостижимые объекты
  • Работает параллельно, паузы минимальны
  • Настройка через GOGC и GOMEMLIMIT
  • Лучший способ помочь GC — писать эффективный код с минимумом аллокаций

В большинстве случаев достаточно понимать базовые принципы и следовать best practices. Тонкая настройка нужна только для высоконагруженных систем с особыми требованиями к производительности или потреблению памяти.