Стек и куча: где программа хранит свои данные и почему это важно
У каждой работающей программы есть два склада для данных. Один — быстрый и аккуратный, как стопка тарелок. Другой — огромный и свободный, но требует ручного учёта. От того, куда попадают ваши данные, зависит скорость и надёжность программы.
Вся память программы делится на две зоны с разными правилами: стек растёт и сжимается сам, а кучей вы распоряжаетесь вручную.
Стек — это стопка тарелок: берёте и кладёте только сверху. Куча — это склад: место найдётся где угодно, но за порядком следить придётся самому.
Когда программа запускается, операционная система выделяет ей кусок памяти. Внутри этого куска есть несколько областей, но два героя нашего рассказа — стек и куча. Разница между ними объясняет добрую половину ошибок и тормозов, с которыми сталкиваются программисты.
Стек: дисциплина стопки тарелок
Стек устроен по принципу «последним пришёл — первым ушёл». Каждый раз, когда вызывается функция, на стек кладётся новый кадр (frame): в нём живут локальные переменные функции и адрес, куда вернуться. Функция закончила работу — её кадр снимается со стека целиком, мгновенно.
Именно поэтому стек невероятно быстрый. Чтобы выделить место, процессору достаточно сдвинуть один указатель — вершину стека. Никакого поиска свободного места, никакого учёта. Положили тарелку, забрали тарелку.
def b():
y = 2 # кадр b: переменная y
return y
def a():
x = 1 # кадр a: переменная x
return b() # поверх кадра a ложится кадр b
print(a())Пока работает b, на стеке лежат оба кадра. Как только b вернула значение, её кадр исчезает, и остаётся только a. Размер стека ограничен — обычно несколько мегабайт. Если переусердствовать (например, бесконечной рекурсией), стек переполнится, и программа рухнет с ошибкой «stack overflow».
Куча: свобода со своей ценой
А что, если вам нужны данные, которые должны пережить функцию, где они созданы? Или объект, размер которого вы узнаёте только во время работы? Тогда на сцену выходит куча — большая область памяти, из которой можно «откусить» блок нужного размера в любой момент.
Куча гораздо просторнее стека и гибче: данные в ней живут ровно столько, сколько нужно, независимо от вызовов функций. Но за гибкость приходится платить. Во-первых, выделение места в куче медленнее — нужно найти подходящий свободный кусок. Во-вторых, кто-то должен освобождать память, когда она больше не нужна.
Кто убирает за нами
Здесь языки расходятся. В C и C++ вы запрашиваете память вручную и так же вручную возвращаете. Забыли вернуть — получите утечку памяти: программа держит блоки, которыми уже не пользуется, и постепенно съедает всю память.
В Python, Java, JavaScript и многих других языках за вас следит сборщик мусора — фоновый механизм, который находит данные, на которые больше никто не ссылается, и освобождает их. Удобно, но не бесплатно: сборка мусора иногда подъедает производительность в неподходящий момент.
Что куда попадает
| Стек | Куча |
| Локальные переменные функций | Объекты и большие структуры |
| Аргументы и адреса возврата | Данные с заранее неизвестным размером |
| Живёт, пока работает функция | Живёт, пока на неё кто-то ссылается |
| Очень быстро, автоматически | Медленнее, требует учёта |
Почему это важно вам
Понимание этих двух зон объясняет реальные вещи. Почему рекурсия может «уронить» программу? Кончился стек. Почему программа со временем начинает тормозить и съедать память? Утечка в куче. Почему в одних языках вы не думаете об освобождении памяти, а в других — постоянно? Разные подходы к управлению кучей.
Большинству программистов не нужно вручную раскладывать данные по стеку и куче — язык делает это за них. Но знать, где живут ваши данные, — значит понимать, почему программа ведёт себя именно так, а не иначе. Это разница между «магия сломалась» и «ага, кончился стек».