Рантайм — справочник
Планировщик G-M-P, стеки горутин, аллокатор, сборка мусора, GOGC и GOMEMLIMIT, escape analysis, pprof и диагностика.
Собрано из A Guide to the Go Garbage Collector, Diagnostics, Profiling Go Programs, комментариев в исходниках рантайма Go 1.27 и release notes.
1. Бинарник и рантайм
go build собирает исполняемый файл, в который уже включён рантайм: виртуальной машины и JIT нет. Рантайм даёт планировщик горутин, сборщик мусора, аллокатор, реализацию каналов и мап, отражение, паники и трассировки.
Без cgo бинарник статический: его можно перенести на машину без Go и без системных библиотек. С cgo появляется динамическая зависимость от libc. Кросс-компиляция задаётся GOOS и GOARCH, например GOOS=linux GOARCH=amd64 go build.
| Что входит в бинарник | Зачем |
|---|---|
| код программы | пакеты и main |
| рантайм | планировщик, GC, аллокатор, каналы, отражение |
| таблицы типов и метаданные | интерфейсы, отражение, трассировки |
| отладочная информация | имена функций и строки; убирается -ldflags="-s -w" |
Пустая программа на Go всё равно весит мегабайты: рантайм и метаданные входят в каждый бинарник. CGO_ENABLED=0 фиксирует статическую сборку.
2. Планировщик: G, M, P
Планировщик распределяет готовые к выполнению горутины по рабочим потокам. Три сущности: G — горутина, M — рабочий поток ОС, P — ресурс, необходимый для выполнения кода Go. Поток M выполняет код Go, только пока владеет P; в системном вызове он может отдать P другому потоку.
Дословно из runtime/proc.go: «G - goroutine. M - worker thread, or machine. P - processor, a resource that is required to execute Go code. M must have an associated P to execute Go code, however it can be blocked or in a syscall w/o an associated P.»
| Механизм | Что делает |
|---|---|
локальная очередь P | готовые горутины, которые создал или разбудил этот P |
| глобальная очередь | общая очередь, когда локальная переполнена |
| work stealing | простаивающий P забирает часть работы у другого |
| sysmon | фоновый монитор: таймеры, вытеснение, возврат занятых P |
| netpoller | горутины, ждущие сеть, паркуются и будятся по готовности |
| асинхронное вытеснение | долгая горутина прерывается сигналом, а не только в точках вызова |
GOMAXPROCS задаёт число P, то есть сколько кода Go может выполняться одновременно. С Go 1.25 на Linux учитывается лимит CPU bandwidth у cgroup, и значение периодически пересчитывается; ручная установка это отключает.
fmt.Println(runtime.NumCPU()) // логические CPU
fmt.Println(runtime.GOMAXPROCS(0)) // текущее значение, без изменения
runtime.Gosched() // уступить процессор другим горутинам
Горутина, заблокированная на канале, мьютексе или сети, не занимает поток ОС: она паркуется, а M берёт другую работу. Число потоков ОС может превышать GOMAXPROCS, например из-за системных вызовов и cgo.
3. Стеки горутин
Горутина начинает с маленького стека: stackMin = 2048 байт в исходниках рантайма. Стек растёт по мере надобности: рантайм выделяет новый участок, копирует туда содержимое и правит указатели. Поэтому адрес локальной переменной не стабилен между вызовами, а указатели на стек не передаются наружу.
| Параметр | Значение |
|---|---|
| начальный стек | 2 КиБ |
| максимальный стек, 64-бит | 1 ГБ |
| максимальный стек, 32-бит | 250 МБ |
| изменение лимита | debug.SetMaxStack |
buf := make([]byte, 1<<20)
n := runtime.Stack(buf, false) // стек текущей горутины
fmt.Println(string(buf[:n]))
n = runtime.Stack(buf, true) // стеки всех горутин
Глубокую рекурсию в Go ограничивает не размер стека по умолчанию, а лимит в 1 ГБ: превышение даёт фатальную ошибку goroutine stack exceeds 1000000000-byte limit.
4. Аллокатор памяти
Аллокатор работает страницами по 8192 байта. Маленькие объекты (до 32 КиБ) округляются до одного из примерно 70 классов размеров; каждая страница делится на объекты одного класса и управляется битовой картой свободных мест.
| Структура | Роль |
|---|---|
mspan | непрерывный участок страниц, нарезанный на объекты одного класса |
mcache | кэш mspan у каждого P; мелкие аллокации идут без блокировки |
mcentral | списки mspan нужного класса с свободными местами |
mheap | куча: страницы и крупные объекты |
Мелкий объект берётся из mcache текущего P. Если в mspan нет свободных мест, P получает новый mspan у mcentral; если и там пусто — у mheap; если куча пуста, рантайм запрашивает у ОС участок не меньше 1 МиБ. Крупные объекты идут напрямую в mheap, минуя кэши. Объекты без указателей и меньше 16 байт обслуживает tiny-аллокатор: несколько мелких значений пакуются в один блок.
С Go 1.27 компилятор генерирует вызовы специализированных по размеру аллокаторов: мелкие выделения (меньше 80 байт) ускорились до 30%, бинарник вырос примерно на 60 КиБ.
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(m.HeapAlloc, m.HeapSys, m.NumGC)
5. Сборка мусора
GC в Go — конкурентная mark-sweep сборка с write barrier. Она не перемещает объекты и не делит их на поколения; аллокация идёт через раздельные по P кэши.
Дословно из runtime/mgc.go: «It is a concurrent mark and sweep that uses a write barrier. It is non-generational and non-compacting. Allocation is done using size segregated per P allocation caches.»
| Фаза | Что происходит | Останавливает мир |
|---|---|---|
| sweep termination | завершается разметка предыдущего цикла, включается write barrier | да, коротко |
| mark | обход достижимых объектов параллельно с программой | нет |
| mark termination | доразметка корней, выключение write barrier | да, коротко |
| sweep | освобождение неотмеченных объектов, часто лениво при аллокации | нет |
Разметка трёхцветная: белые объекты ещё не встречены, серые поставлены в очередь, чёрные просканированы. Write barrier при записи указателя в чёрный объект «затеняет» новую цель, чтобы чёрный объект не начал ссылаться на белый и тот не был ошибочно освобождён.
Корни — стеки горутин, глобальные переменные и регистры. Пока цикл разметки идёт, горутина, которая много аллоцирует, помогает GC: это называется GC assist. Стоимость сборки — процессорное время и задержки, а не только паузы.
6. GOGC и GOMEMLIMIT
GOGC задаёт целевой размер кучи после цикла. При значении 100 цель — удвоить живую кучу с корнями; чем больше GOGC, тем реже сборки и тем больше памяти. Формула из руководства:
Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100
Пример: живая куча 8 МиБ, стеки 1 МиБ, глобальные указатели 1 МиБ. При GOGC=100 до следующей сборки можно выделить ещё 10 МиБ; при 50 — 5 МиБ; при 200 — 20 МиБ. Удвоение GOGC удваивает память и примерно вдвое снижает процессорную стоимость GC.
GOMEMLIMIT (Go 1.19) задаёт мягкий предел памяти рантайма: Sys - HeapReleased, или /memory/classes/total:bytes - /memory/classes/heap/released:bytes в терминах runtime/metrics. Это не жёсткий лимит: рантайм стремится уложиться, но при нехватке памяти программа всё равно может получить OOM.
| Настройка | Переменная | API |
|---|---|---|
| цель кучи | GOGC | debug.SetGCPercent |
| отключить GC | GOGC=off | debug.SetGCPercent(-1) |
| мягкий предел памяти | GOMEMLIMIT | debug.SetMemoryLimit |
| принудительный цикл | — | runtime.GC |
| вернуть память ОС | — | debug.FreeOSMemory |
import "runtime/debug"
debug.SetGCPercent(200) // реже сборки, больше памяти
debug.SetMemoryLimit(512 << 20) // мягкий предел 512 МиБ
debug.SetGCPercent(-1) // GC выключен
// в тестах и на старте удобно смотреть циклы:
// GODEBUG=gctrace=1 ./app
7. Escape analysis
Компилятор решает, где жить значению: на стеке или в куче. Если значение не выходит за пределы функции и его адрес не сохраняется, оно остаётся на стеке. Если «убегает» — компилятор выделяет его в куче, и дальше им занимается GC.
Проверить решение компилятора: go build -gcflags=-m ./.... Флаг -m -m печатает больше причин. Строка escapes to heap означает, что значение ушло в кучу.
type Point struct{ X, Y int }
func NewPoint() *Point {
return &Point{} // escapes to heap: указатель возвращается наружу
}
func Sum() int {
p := Point{X: 1, Y: 2} // не escapes: p не покидает функцию
return p.X + p.Y
}
| Причина | Пример |
|---|---|
| возврат указателя | return &v |
| упаковка в интерфейс | var anyV any = v |
| захват замыканием, которое живёт дольше | func() { use(v) }, сохранённая в поле |
| отправка в канал | ch <- v |
| запись в глобальную переменную | global = &v |
| слишком большой объект | размер известен, но превышает лимит стека |
| неизвестный размер | make([]T, n) с переменным n |
Escape analysis — оптимизация, а не контракт: полагаться на конкретное решение компилятора нельзя. Если аллокаций слишком много, их ищут профилем кучи, а не догадками.
8. Профилирование
runtime/pprof отдаёт данные в формате, который понимает go tool pprof. Профиль можно снять в тесте, включить HTTP-эндпоинтами или собрать вручную.
| Профиль | Что показывает | Как включить |
|---|---|---|
cpu | где программа тратит процессорное время | go test -cpuprofile, pprof.StartCPUProfile |
heap | выделения и текущая память | go test -memprofile, pprof.WriteHeapProfile |
goroutine | стеки всех текущих горутин | всегда доступен |
threadcreate | создание потоков ОС | всегда доступен |
block | блокировки на примитивах синхронизации | runtime.SetBlockProfileRate |
mutex | конкуренция за мьютексы | runtime.SetMutexProfileFraction |
goroutineleak | горутины, заблокированные навсегда | общедоступен с Go 1.27 |
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ...
}
Снятие и разбор:
go test -bench . -cpuprofile cpu.out -memprofile mem.out
go tool pprof -http=:8080 cpu.out
go tool pprof -alloc_space mem.out # все выделения
go tool pprof -inuse_space mem.out # что живо сейчас
go tool trace показывает не агрегаты, а временную шкалу: когда горутины выполнялись, блокировались, ждали GC и сеть. Трассировка полезна для задержек, профиль — для стоимости.
go test -trace trace.out
go tool trace trace.out
Профилирование в production допустимо, но CPU-профиль добавляет накладные расходы. Block- и mutex-профили по умолчанию выключены, потому что сами стоят производительности.
9. Диагностика
| Инструмент | Что даёт |
|---|---|
GODEBUG=gctrace=1 | событие каждой сборки: объём, паузы |
GODEBUG=schedtrace=X | состояние планировщика каждые X мс |
GODEBUG=inittrace=1 | время и аллокации инициализации пакетов |
runtime/metrics | стабильные именованные метрики рантайма |
runtime.ReadMemStats | снимок памяти и числа сборок |
expvar | публикация переменных через HTTP в JSON |
go tool nm, go tool objdump | символы и дизассемблер бинарника |
import "runtime/metrics"
samples := []metrics.Sample{
{Name: "/memory/classes/heap/objects:bytes"},
{Name: "/gc/cycles/total:gc-cycles"},
{Name: "/sched/goroutines:goroutines"},
}
metrics.Read(samples)
for _, s := range samples {
fmt.Println(s.Name, s.Value.Uint64())
}
runtime/metrics — стабильный интерфейс: имена метрик не меняются от версии к версии так, как внутренние поля MemStats. Для дашбордов и алертов лучше он.
10. Первоисточники
- A Guide to the Go Garbage Collector — mark-sweep, GOGC, GOMEMLIMIT, стоимость GC
- Diagnostics — профили, трассировка, отладка, статистика рантайма
- Profiling Go Programs — практика
pprof - pkg.go.dev/runtime —
GOMAXPROCS,MemStats,Stack,SetBlockProfileRate - pkg.go.dev/runtime/metrics — имена и описание метрик
- pkg.go.dev/runtime/debug —
SetGCPercent,SetMemoryLimit,SetMaxStack - pkg.go.dev/runtime/pprof — профили и их включение
- Release notes Go 1.25 — container-aware
GOMAXPROCS - Release notes Go 1.26 — рандомизация базы кучи, профиль
goroutineleak - Release notes Go 1.27 — специализированные по размеру аллокаторы,
goroutineleakGA - runtime/proc.go — комментарий планировщика G, M, P
- runtime/mgc.go — фазы GC и write barrier
- runtime/malloc.go — устройство аллокатора