День 4: Garbage Collector, профилирование и оптимизации
🎯 Темы дня
- Как работает GC в Go (tri-color mark-and-sweep)
- GOGC и GOMEMLIMIT — настройка GC
- Профилирование памяти: pprof, heap dumps
- Практические оптимизации: sync.Pool, pre-allocation, zero-copy
📖 1. Garbage Collector — как работает
Алгоритм: Tri-color Mark-and-Sweep
Go использует concurrent, non-generational, tri-color mark-and-sweep GC.
Цвета объектов:
- Белый — не проверен, потенциальный мусор
- Серый — обнаружен, но потомки не проверены
- Чёрный — проверен, потомки тоже проверены (живой)
Алгоритм:
- Mark phase (concurrent):
- Найти все корни (stack, globals, финализаторы).
- Пометить их как серые.
- Для каждого серого объекта: пометить как чёрный, все его потомки — как серые.
- Повторять, пока нет серых объектов.
- Sweep phase (concurrent):
- Все белые объекты — мусор, освободить память.
Write Barrier
Чтобы GC работал concurrently с мутациями, используется write barrier — специальная инструкция, которая уведомляет GC об изменении указателей.
Стоимость: Небольшая overhead на каждую запись указателя.
Когда запускается GC
GC запускается, когда:
- Размер heap вырос в
GOGCраз с последней сборки (по умолчанию 100%, т.е. удвоение). - Достигнут
GOMEMLIMIT(если задан). - Принудительно через
runtime.GC().
Цель GC
Главная цель Go GC — низкая latency, а не высокий throughput.
- Цель: пауза < 500 микросекунд (обычно < 100 мкс).
- Жертва: может потреблять больше памяти, чем нужно.
Цикл целиком: два коротких STW-окна — Sweep Termination и Mark Termination — обрамляют конкурентные фазы Mark и Sweep. Пока программа продолжает работать, write barrier помогает GC раскрашивать объекты, поэтому «пауза» — это доли миллисекунды, а не весь цикл.
📖 2. GOGC и GOMEMLIMIT
GOGC
Определяет, насколько может вырасти heap перед запуском GC.
bashGOGC=100 # по умолчанию: GC запускается, когда heap удвоился GOGC=200 # GC запускается, когда heap вырос в 3 раза GOGC=50 # GC запускается чаще, heap растёт медленнее
Когда менять:
GOGC=200— если много памяти и хочешь реже запускать GC.GOGC=50— если мало памяти и хочешь экономить.
GOMEMLIMIT (Go 1.19+)
Жёсткий лимит памяти. GC будет запускаться чаще, чтобы не превысить лимит.
bashGOMEMLIMIT=1GiB # не превышать 1 GB
Важно: Это мягкий лимит. Go может временно превысить его, но будет стараться вернуться.
Когда использовать:
- В контейнерах с ограниченной памятью (Kubernetes).
- Чтобы избежать OOM killer.
📖 3. Профилирование памяти
pprof — основной инструмент
Подключить pprof в код:
goimport _ "net/http/pprof" func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() // ... }
Снять heap profile:
bashgo tool pprof http://localhost:6060/debug/pprof/heap
Команды в pprof:
(pprof) top 10 # топ-10 функций по аллокациям
(pprof) list <func> # показать аллокации в функции
(pprof) web # графический вид (нужен graphviz)
(pprof) traces # трассировка аллокаций
Поиск утечек памяти
Признаки утечки:
- Heap растёт монотонно.
- GC не освобождает память.
- In-use space растёт.
Как найти:
- Снять два heap profile с интервалом:
bash
curl -o heap1.out http://localhost:6060/debug/pprof/heap sleep 30 curl -o heap2.out http://localhost:6060/debug/pprof/heap - Сравнить:
bash
go tool pprof --base heap1.out heap2.out - Посмотреть, что аллоцируется и не освобождается.
MemStats
goimport "runtime" var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("Alloc = %v MB\n", m.Alloc/1024/1024) fmt.Printf("TotalAlloc = %v MB\n", m.TotalAlloc/1024/1024) fmt.Printf("Sys = %v MB\n", m.Sys/1024/1024) fmt.Printf("NumGC = %v\n", m.NumGC)
📖 4. Практические оптимизации
sync.Pool — переиспользование объектов
govar bufPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func process() { buf := bufPool.Get().(*bytes.Buffer) defer bufPool.Put(buf) buf.Reset() // используем buf }
Зачем: Избежать аллокаций в hot path, снизить нагрузку на GC.
Когда использовать:
- Частые аллокации однотипных объектов (buffers, temporary structs).
- В HTTP handlers, где много запросов.
Когда НЕ использовать:
- Если объекты живут долго.
- Если аллокации редкие.
Pre-allocation слайсов
go// ❌ Плохо: много аллокаций при append s := make([]int, 0) for i := 0; i < 1000; i++ { s = append(s, i) } // ✅ Хорошо: одна аллокация s := make([]int, 0, 1000) for i := 0; i < 1000; i++ { s = append(s, i) }
strings.Builder для конкатенации
go// ❌ Плохо: O(N²) s := "" for i := 0; i < 10000; i++ { s += "a" } // ✅ Хорошо: O(N) var builder strings.Builder builder.Grow(10000) for i := 0; i < 10000; i++ { builder.WriteString("a") } s := builder.String()
Zero-copy конверсии (unsafe)
goimport "unsafe" func stringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } func bytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) }
Опасно: Строка иммутабельна, слайс — нет. Если изменить слайс — UB.
Когда использовать:
- High-performance код, где критичны аллокации.
- Только если уверен, что слайс не будет изменён.
💻 Практика (1–2 часа)
Задание 1: Профилирование heap
Напиши код, который аллоцирует много памяти, и найди утечку через pprof:
govar leaks []*bytes.Buffer func leak() { buf := bytes.NewBuffer(make([]byte, 1024*1024)) // 1 MB leaks = append(leaks, buf) } func main() { go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() for i := 0; i < 100; i++ { leak() time.Sleep(100 * time.Millisecond) } time.Sleep(time.Hour) }
Сними heap profile и найди, где аллоцируется память.
Задание 2: sync.Pool
Реализуй pool для bytes.Buffer и сравни производительность с аллокацией каждый раз:
govar bufPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func BenchmarkWithPool(b *testing.B) { for i := 0; i < b.N; i++ { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() buf.WriteString("hello") bufPool.Put(buf) } } func BenchmarkWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { buf := new(bytes.Buffer) buf.WriteString("hello") } }
Задание 3: Оптимизация аллокаций
Найди и исправь лишние аллокации в этом коде:
gofunc processUsers(users []User) []string { var names []string for _, u := range users { names = append(names, u.Name) } return names }
Исправление:
gofunc processUsers(users []User) []string { names := make([]string, 0, len(users)) // pre-allocation for _, u := range users { names = append(names, u.Name) } return names }
🗣️ Каверзные вопросы
-
Как работает GC в Go? Ответ: Tri-color mark-and-sweep, concurrent, non-generational. Цель — низкая latency.
-
Что такое GOGC? Ответ: Процент роста heap, при котором запускается GC. По умолчанию 100% (удвоение).
-
Что такое GOMEMLIMIT? Ответ: Жёсткий лимит памяти. GC будет запускаться чаще, чтобы не превысить лимит.
-
Как найти утечку памяти? Ответ: Снять два heap profile с интервалом, сравнить через
pprof --base. -
Что такое sync.Pool? Ответ: Пул для переиспользования объектов, чтобы избежать аллокаций в hot path.
-
Почему
strings.Builderбыстрее конкатенации? Ответ: Использует внутренний буфер с амортизированным ростом, избегая O(N²) копирований. -
Что опасного в zero-copy конверсии
string↔[]byte? Ответ: Строка иммутабельна, слайс — нет. Если изменить слайс — UB.
🧩 Проверь себя
🏁 Финальный чек-лист курса
Теория
- Понимаю, как устроен стек горутины и как он растёт
- Знаю, зачем нужен heap и как в нём аллоцируется память
- Могу запустить escape analysis и объяснить его вывод
- Понимаю alignment и padding в структурах
- Умею оптимизировать порядок полей для уменьшения размера
- Знаю, когда использовать value, а когда pointer
- Понимаю method sets для value vs pointer receivers
- Знаю, как работает GC (tri-color mark-and-sweep)
- Понимаю GOGC и GOMEMLIMIT
Практика
- Умею профилировать память через pprof
- Могу найти утечку памяти
- Использую sync.Pool для оптимизации
- Применяю pre-allocation слайсов
- Использую strings.Builder для конкатенации
- Понимаю риски zero-copy конверсий
Топ-10 вопросов, которые точно спросят
- Чем stack отличается от heap?
- Что такое escape analysis?
- Почему размер структуры не равен сумме полей?
- Как оптимизировать размер структуры?
- Когда использовать pointer, а когда value?
- Что такое method sets?
- Как работает GC в Go?
- Что такое GOGC и GOMEMLIMIT?
- Как найти утечку памяти?
- Что такое sync.Pool и когда его использовать?
Удачи! 🚀