Как в 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.

Ответ 18+ 🔞

А, ну вот, опять про стек и кучу, классика! Слушай, давай разберём эту песню без соплей, как есть.

Представь себе, что твоя программа — это такой огромный, ёбаный, многоквартирный дом. И в этом доме есть два принципиально разных способа жить.

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

Куча — это общая, ёпта, помойка во дворе этого дома. Общая на всех! Сюда ты выносишь всё, что не влезло в комнату или должно пережить твой уход. Глобальные переменные, объекты через make и new — всё это валяется тут. Взять что-то отсюда или выбросить — процесс долгий, муторный, потому что нужно к общей куче идти, с мусорщиком (GC) договариваться, который только тут и работает. Он ходит, смотрит: "Ага, на эту переменную уже никто не ссылается? В топку её, нахуй!".

А теперь самое интересное — Анализ Побега (Escape Analysis). Это такой хитрожопый алгоритм в компиляторе Go, который решает: "Мужик, ты эту свою локальную переменную из комнаты вынесешь на общую помойку или нет?".

Смотри, пример:

// Функция, которая создаёт проблему.
func createHeapObject() *int {
    x := 42 // Родился 'x' в комнате (стеке).
    return &x // Ага! Ты возвращаешь НА НЕГО УКАЗАТЕЛЬ наружу!
}

Компилятор смотрит на это и орет: "Ёбаный насос! На переменную x теперь будет ссылка снаружи функции! Он 'сбежал'! Если я оставлю его в комнате (стеке), а комната после функции очистится — внешний указатель будет тыкать в хуй пойми что! Пиздец, segmentation fault! Вали его, блядь, в кучу!".

И x летит на общую помойку (heap).

А теперь другой случай:

// Функция, которая никого не палит.
func createStackObject() int {
    y := 100 // 'y' в комнате.
    return y // Возвращаем ЗНАЧЕНИЕ, копию цифры 100.
}

Компилятор: "О, спокойно. Возвращаешь копию значения. Сам y после функции помрёт, и всем похуй. Оставляем его в комнате (стеке), всё чики-пуки.".

А как же устроена сама куча, эта помойка-то?

А там, блядь, целая бюрократия! Не просто свалка, а система.

  1. mcache — это твой личный, быстрый карман. У каждого потока (P) свой. Мелкие объекты (меньше 32KB) ты сразу оттуда хватаешь и туда же возвращаешь, без всяких очередей и блокировок. Быстро, как из стека почти.
  2. mcentral — это уже общий склад для объектов одного размера (размерных классов). Если в личном кармане (mcache) пусто — идешь сюда, но тут уже могут быть очереди.
  3. mheap — это главная, ёбта, свалка. Сюда идут все крупные объекты (>32KB) и отсюда берутся новые страницы памяти для складов (mcentral).

Вот так вот, не просто "память есть память", а целое министерство распределения, ебать его в сраку. Но в целом, если пишешь нормально, компилятор эту хуйню сам оптимизирует, чтобы поменьше в кучу летело. Главное — понимать, когда твой код заставляет его орать "Всё, пиздец, бегит!" и отправлять переменные на общую помойку.