← Назад к вопросам

Как устроена куча?

2.4 Senior🔥 62 комментариев
#Производительность и оптимизация

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Устройство кучи (heap) в Go

Куча (heap) — это область динамической памяти, управляемая сборщиком мусора (Garbage Collector, GC), где размещаются объекты с неопределённым временем жизни или те, которые не могут быть размещены на стеке. В Go куча — это неупорядоченная структура данных, а общий термин для динамически выделяемой памяти.

Основные принципы работы кучи в Go

  1. Динамическое выделение памяти Куча используется, когда компилятор не может определить время жизни объекта во время компиляции или когда объект слишком велик для стека. Например:

    func createObject() *MyStruct {
        // Выделение в куче, так как указатель возвращается из функции
        return &MyStruct{Value: 42}
    }
    
  2. Управление через сборщик мусора В отличие от стека (где память освобождается автоматически при выходе из функции), память в куче освобождается сборщиком мусора. Go использует неконкурентный триколорный маркировочно-подметающий алгоритм (начиная с версии 1.5).

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

    • Классы размеров (size classes): 68 фиксированных размеров от 8 байт до 32 КБ
    • Большие объекты (>32 КБ) выделяются отдельно
    • Списки свободных блоков (free lists) для быстрого выделения

Процесс выделения памяти в куче

type Data struct {
    items []int
}

func process() {
    // Этот объект будет размещён в куче
    d := &Data{items: make([]int, 1000)}
    
    // Слайс также размещается в куче, так как его базовый массив динамический
    slice := make([]int, 100)
}

Алгоритм выделения:

  1. Проверка локальных кэшей потоков (mcache) для соответствующего класса размеров
  2. Если нет свободных блоков — запрос к центральному кэшу (mcentral)
  3. При необходимости — запрос новых участков (spans) у глобального аллокатора (mheap)

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

Три фазы работы GC в Go:

  1. Фаза маркировки (Mark phase)

    • Сканирование корневых объектов (глобальные переменные, стек, регистры)
    • Рекурсивное помечение достижимых объектов
  2. Фаза завершения маркировки (Mark termination)

    • Остановка программы (STW - Stop The World), но очень краткий
    • Завершение маркировки
  3. Фаза подметания (Sweep phase)

    • Освобождение непомеченных объектов
    • Выполняется параллельно с работой программы
// Пример, демонстрирующий работу GC
func memoryIntensive() {
    var objects []*bigObject
    
    for i := 0; i < 1000; i++ {
        // Создание объектов в куче
        obj := &bigObject{data: make([]byte, 1024*1024)} // 1MB
        
        // Часть объектов становится недостижимой
        if i%2 == 0 {
            objects = append(objects, obj) // Сохраняем указатель
        }
        // obj с нечётными i станет кандидатом на сборку мусора
    }
}

Оптимизации кучи в Go

Эскейп-анализ (escape analysis) Компилятор Go анализирует, может ли объект быть размещён на стеке или должен "сбежать" в кучу:

func safe() int {
    x := 42 // Размещается на стеке
    return x
}

func escapes() *int {
    x := 42 // "Сбегает" в кучу, так как возвращается указатель
    return &x
}

Поколенческая организация Хотя Go не использует классические поколения, куча разделена на:

  • Молодые объекты — чаще становятся мусором
  • Старые объекты — с большей вероятностью остаются в памяти

Управление кучей и производительность

Проблемы:

  • Фрагментация памяти — минимизируется классами размеров
  • Паузы GC — сокращены до субмиллисекундных в современных версиях
  • Потребление памяти — Go может возвращать память ОС, но делает это консервативно

Оптимизации:

  1. Использование sync.Pool для объектов с коротким временем жизни
  2. Предварительное выделение слайсов и мап
  3. Локальные переменные вместо глобальных, когда возможно
  4. Контроль за утечками указателей на устаревшие объекты

Сравнение со стеком

ХарактеристикаСтекКуча
Скорость выделенияБыстрее (указатель стека)Медленнее (поиск свободного блока)
Управление памятьюАвтоматическое (LIFO)Через сборщик мусора
ФрагментацияНетВозможна
ПотокобезопасностьУ каждого потока свой стекОбщая для всех горутин
РазмерОграничен (обычно несколько МБ)Ограничен доступной памятью ОС

Практические рекомендации

  1. Не бойтесь аллокаций в куче, но понимайте их стоимость
  2. Используйте профилирование (pprof, trace) для анализа памяти
  3. Оптимизируйте только после измерений — преждевременная оптимизация может ухудшить читаемость
  4. Следите за размерами структур — выравнивание полей может сократить потребление памяти
// Пример оптимизации памяти
type Optimized struct {
    a int32
    b int32
    c int8
    // 8 байт вместо 12 при неправильном порядке
}

type Unoptimized struct {
    a int8
    b int32
    c int32
    // 12 байт из-за выравнивания
}

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