Стек и куча: что нужно знать каждому разработчику
Основы3 минРедакция Innovialabs
Коротко
Стек — быстрая память для локальных данных функции, которая освобождается сама при выходе из неё; куча — гибкая память для данных с произвольным временем жизни, но выделение в ней дороже.
Коротко: в чём разница
У программы есть две основные области памяти для данных:
- Стек (stack) — хранит локальные переменные и служебную информацию вызова функции. Выделение и освобождение почти бесплатны: при входе в функцию создаётся кадр (frame), при выходе он исчезает целиком.
- Куча (heap) — хранит данные, которые должны жить дольше вызова функции или чей размер известен только во время работы. Память здесь выделяется явно (
new,malloc) или неявно, а освобождается вручную, сборщиком мусора или по правилам владения.
| Стек | Куча | |
|---|---|---|
| Время жизни | Пока выполняется функция | Пока данные нужны |
| Скорость выделения | Очень высокая | Ниже |
| Размер | Ограничен, задаётся ОС и настройками | Намного больше |
| Кто освобождает | Автоматически при выходе | GC, программист или владелец |
| Доступ | Один поток — свой стек | Общая для всех потоков |
Как работает стек
Стек работает по принципу LIFO — последним пришёл, первым ушёл. Когда функция a вызывает b, кадр b кладётся поверх кадра a. Когда b возвращается, её кадр снимается, и все её локальные переменные пропадают.
Поэтому нельзя возвращать указатель на локальную переменную в C: после выхода из функции эта память уже чужая.
Как работает куча
Куча — большой пул памяти, из которого аллокатор выдаёт блоки нужного размера. Это гибко, но цена такая:
- аллокатору нужно найти свободный блок;
- память может фрагментироваться;
- в языках с GC каждая аллокация — будущая работа для сборщика;
- объекты разбросаны по памяти, что хуже для кэша процессора.
Значимые и ссылочные типы
Популярное упрощение «значимые типы на стеке, ссылочные в куче» неточно. Точнее так: значимый тип хранит сами данные, ссылочный — адрес данных в куче. Где окажется значение, зависит от того, где оно объявлено.
- Java: примитивы (
int,double) — значения; объекты — в куче, переменная хранит ссылку. JIT может избежать выделения в куче через escape analysis. - C#:
struct— значимый тип,class— ссылочный. Ноstructвнутри поля класса живёт в куче вместе с объектом. - Go: компилятор сам решает, где разместить значение, через escape analysis. Если значение «убегает» из функции (например, на него возвращается указатель), оно уходит в кучу.
- Python и JavaScript: почти всё — объекты в куче, детали размещения скрыты движком.
Практическое следствие — разная семантика копирования:
struct PointS { public int X; }
class PointC { public int X; }
var a = new PointS { X = 1 }; var b = a; b.X = 2; // a.X == 1, скопировано значение
var c = new PointC { X = 1 }; var d = c; d.X = 2; // c.X == 2, скопирована ссылка
В Go можно посмотреть решения компилятора:
go build -gcflags=-m ./...
Stack overflow
Стек ограничен, поэтому слишком глубокая рекурсия или огромные локальные массивы его переполняют. Выглядит это по-разному:
- Java —
StackOverflowError; - JavaScript —
RangeError: Maximum call stack size exceeded; - Python —
RecursionError(интерпретатор ограничивает глубину заранее); - C/C++ — обычно аварийное завершение процесса.
Как лечить: переписать рекурсию в цикл, использовать явную структуру данных (свой стек в куче), не хранить большие массивы в локальных переменных.
Почему это важно для производительности
- Меньше аллокаций в куче — меньше работы для GC и короче паузы.
- Переиспользуйте буферы в горячих циклах вместо создания новых объектов на каждой итерации.
- Компактные данные быстрее: массив значений обходится эффективнее, чем массив ссылок на разбросанные объекты.
- Не оптимизируйте вслепую. Сначала профилировщик, потом изменения.