Вопрос с собеседования · Основы

Чем стек отличается от кучи?

JuniorОперационные системы
Ещё спрашивают так:
  • Где хранятся локальные переменные и объекты?
  • Что такое StackOverflowError?
  • Почему выделение на стеке быстрее, чем в куче?
Коротко

Стек — память для вызовов функций: локальные переменные и адрес возврата, выделяется и освобождается автоматически при входе и выходе. Куча — память для данных, чьё время жизни не привязано к вызову.

  • Стек у каждого потока свой, небольшой (обычно 1–8 МБ), работает как LIFO
  • Выделение на стеке — сдвиг указателя, почти бесплатно
  • Куча общая для потоков; освобождает программист или сборщик мусора

А ещё Глубокая рекурсия переполняет стек — StackOverflowError, RecursionError

Попробуйте сами

Дерево рекурсивных вызовов и мемоизация

Подробно

Стек вызовов. При вызове функции на стек кладётся кадр (stack frame): аргументы, локальные переменные, адрес возврата. При выходе кадр снимается. Память выделяется и освобождается сдвигом одного указателя, поэтому это очень быстро и не фрагментирует память. Ограничения: размер стека фиксирован при создании потока (в Linux по умолчанию 8 МБ для главного потока, в Java -Xss, обычно 512 КБ – 1 МБ), и данные живут только до выхода из функции.

Куча — область для объектов, которые должны пережить вызов или чей размер неизвестен заранее. Выделение сложнее: аллокатор ищет свободный блок подходящего размера, память фрагментируется. Освобождение — вручную (free, delete в C/C++), через подсчёт ссылок (Python, Swift) или сборщиком мусора (Java, Go, C#).

Что где лежит:

  • в C/C++ локальные массивы и структуры — на стеке, malloc/new — в куче;
  • в Java на стеке — примитивы и ссылки, сами объекты — в куче (JIT может разместить объект на стеке после escape-анализа, если он не «убегает» из метода);
  • в Python все объекты в куче, на стеке интерпретатора — ссылки;
  • в Go компилятор сам решает по escape-анализу.

Переполнение стека. Каждый рекурсивный вызов добавляет кадр. Без базового случая или при глубине в десятки тысяч стек кончается: StackOverflowError в Java, RecursionError в Python (там лимит глубины 1000 по умолчанию стоит раньше реального стека). Лечится переписыванием на цикл с явным стеком.

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

Не путать со структурами данных: «куча» как область памяти и двоичная куча (heap) — разные вещи с одинаковым названием.

Пример

import sys
sys.getrecursionlimit()          # 1000

def depth(n):
    return 0 if n == 0 else 1 + depth(n - 1)

depth(500)                       # работает
depth(5000)                      # RecursionError: maximum recursion depth exceeded

def depth_loop(n):               # тот же результат без роста стека
    d = 0
    while n: n -= 1; d += 1
    return d

Что хотят услышать

  • Стек — кадры вызовов, LIFO, автоматическое освобождение, свой у потока.
  • Куча — объекты с произвольным временем жизни, общая, GC или ручное освобождение.
  • Почему стек быстрее и почему он ограничен.
  • Переполнение стека при рекурсии и как его избежать.

Частые ошибки

  • Путать область памяти «куча» со структурой данных «куча».
  • Говорить, что в Java объекты бывают на стеке «если маленькие».

Проверьте себя

Один верный вариант. Подумайте, потом откройте ответ.

Показать варианты и ответ
  • ✗Стек общий для всех потоков, а куча у каждого потока своя
  • ✗Выделение на стеке дороже, чем в куче, поэтому объекты кладут в кучу
  • ✓Стек — для вызовов функций, свой у каждого потока; куча — для данных с независимым временем жизни
  • ✗Размер стека потока не ограничен, поэтому глубокая рекурсия безопасна

Что спросят дальше

Связанные вопросы

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования