Стек вызовов. При вызове функции на стек кладётся кадр (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) — разные вещи с одинаковым названием.