Как в Go устроена память: стек и куча (heap)?

Ответ

В Go, как и в большинстве языков, память для работающей программы делится на две основные области: стек (stack) и кучу (heap).

Стек (Stack)

  • Назначение: Для хранения локальных переменных, аргументов функций и адресов возврата. У каждой горутины есть свой собственный, независимый стек.
  • Структура: LIFO (Last-In, First-Out). Память выделяется и освобождается очень быстро — простым смещением указателя стека.
  • Размер: Изначально небольшой (в Go ~2KB), но может динамически расти и сжиматься по мере необходимости.
  • Управление: Полностью автоматическое и управляется компилятором. Сборщик мусора (GC) не работает со стеком.

Куча (Heap)

  • Назначение: Для хранения данных, время жизни которых не может быть определено на этапе компиляции. Это глобальные переменные и динамически выделяемые объекты (например, через make или new), на которые есть ссылки из других мест.
  • Структура: Общая область памяти для всех горутин. Выделение и освобождение памяти — более сложная и медленная операция.
  • Управление: Управляется сборщиком мусора (GC). GC находит объекты в куче, которые больше не используются, и освобождает занимаемую ими память.

Escape Analysis (Анализ побега)

Ключевой механизм в Go, который определяет, где разместить переменную — на стеке или в куче. Компилятор анализирует код и, если он может доказать, что переменная не будет использоваться после завершения функции (т.е. на неё не будет внешних ссылок), он размещает её на стеке. Это намного эффективнее.

Если же на переменную есть ссылка, которая «убегает» из функции (например, возвращается указатель на локальную переменную), она будет размещена в куче.

Пример:

// Возвращает указатель на локальную переменную.
// 'x' "сбежит" в кучу.
func createHeapObject() *int {
    x := 42
    return &x 
}

// Возвращает значение, а не указатель.
// 'y' останется на стеке, т.к. её копия будет возвращена.
func createStackObject() int {
    y := 100
    return y
}

Внутреннее устройство кучи в Go:

Память в куче управляется через иерархию структур: mheap, mcentral, mcache. Объекты делятся на классы по размеру. Мелкие объекты (<32KB) выделяются очень быстро из кэша, привязанного к процессору (mcache), чтобы избежать блокировок при конкурентном доступе. Крупные объекты выделяются напрямую из mheap.