Структура памяти в Java: стек и куча
Стек (stack) и куча (heap) — две основные области памяти, которыми управляет JVM во время работы программы. В куче живут объекты и массивы, в стеке — кадры вызовов методов с локальными переменными и ссылками на эти объекты. Память в куче освобождает сборщик мусора, память в стеке — сама JVM, как только метод завершился.
Структура памяти в Java устроена сложнее, чем «стек и куча», но на старте достаточно разобраться именно с этими двумя областями: через них объясняется почти всё поведение переменных, объектов и сборщика мусора.
1. Куча (heap)
Java Heap (куча) — область памяти, которую JVM выделяет при старте приложения и использует для размещения всех объектов и массивов. Каждый раз, когда выполняется new, память под новый объект берётся именно из кучи.
Именно в куче работает сборщик мусора (garbage collector): он находит объекты, до которых больше нельзя добраться ни по одной цепочке ссылок, и освобождает занятую ими память. Разработчику не нужно удалять объекты вручную.
Куча одна на всю JVM и общая для всех потоков приложения. Поэтому объект, созданный в одном потоке, может быть прочитан и изменён из другого — и поэтому же общие объекты приходится защищать синхронизацией.
Важное уточнение: объект в куче не «виден отовсюду» сам по себе. Обратиться к нему можно только через ссылку — если ссылку никуда не передали и она пропала, объект становится недостижимым и рано или поздно будет собран сборщиком мусора.
2. Стек (stack)
Стековая память в Java работает по принципу LIFO (last in — first out, «последним зашёл — первым вышел»). При каждом вызове метода на вершине стека создаётся новый кадр стека (stack frame), в котором хранятся:
- локальные переменные метода примитивных типов (их значения),
- ссылки на объекты, лежащие в куче,
- параметры метода и служебная информация для возврата.
Как только метод завершает работу, его кадр снимается со стека, и память освобождается мгновенно — сборщик мусора здесь не участвует.
У каждого потока свой собственный стек. Локальные переменные потока по определению недоступны другим потокам, поэтому они всегда потокобезопасны. Размер стека намного меньше размера кучи: обычно это сотни килобайт — единицы мегабайт на поток.
Ключевое правило: объект всегда создаётся в куче, а в стеке лежит только ссылка на него. Значение ссылочной переменной — это не сам объект, а адрес объекта в куче.
Неочевидный момент
В стеке лежат только локальные примитивы. Примитивные поля объекта (например, int x внутри класса) хранятся вместе с самим объектом — то есть в куче. Правило «примитивы в стеке» работает лишь для переменных внутри метода.
3. Пример: что попадает в стек, а что в кучу
Разберём разницу между примитивной и ссылочной переменной. При объявлении int a = 10 само значение 10 хранится в стеке. При создании Test a = new Test() объект размещается в куче, а переменная a находится в стеке и содержит ссылку на этот объект:

Теперь то же самое в коде:
public class MemoryDemo {
static class Point {
int x; // поле объекта - хранится в куче вместе с объектом
int y;
}
public static void main(String[] args) {
int a = 10; // значение 10 лежит в кадре стека метода main
Point p = new Point(); // объект Point - в куче, ссылка p - в стеке
p.x = 5; // изменяем поле объекта в куче
modify(p); // передаём копию ссылки
System.out.println(a); // 10
System.out.println(p.x); // 42 - метод изменил тот же объект в куче
}
static void modify(Point point) { // новый кадр стека
point.x = 42; // point и p указывают на один объект
} // кадр снимается со стека
} Пример показывает сразу три вещи: значение примитива копируется, ссылка тоже копируется (Java всегда передаёт аргументы по значению), но копия ссылки указывает на тот же самый объект в куче — поэтому изменения полей видны в вызывающем методе.
4. Стек и куча: сравнение
| Критерий | Стек (stack) | Куча (heap) |
|---|---|---|
| Что хранится | Кадры методов: локальные примитивы, ссылки, параметры | Объекты, массивы, поля объектов |
| Кому принадлежит | Свой стек у каждого потока | Одна на всю JVM, общая для всех потоков |
| Время жизни данных | До выхода из метода | Пока на объект есть достижимые ссылки |
| Освобождение памяти | Автоматически, снятием кадра | Сборщиком мусора |
| Размер | Небольшой, задаётся ключом -Xss | Значительно больше, задаётся -Xms / -Xmx |
| Скорость доступа | Очень быстрая | Медленнее: нужен поиск по ссылке и работа GC |
| Ошибка при переполнении | StackOverflowError | OutOfMemoryError: Java heap space |
5. Metaspace и другие области памяти JVM
Стек и куча — не вся картина. Современная JVM (HotSpot) использует ещё несколько областей:
- Metaspace — метаданные загруженных классов и методов. Начиная с Java 8 они хранятся в нативной памяти операционной системы, а не в куче.
- Пул строк (String pool) — с Java 7 находится в куче, поэтому строковые литералы тоже подлежат сборке мусора.
- Code Cache — машинный код, сгенерированный JIT-компилятором.
- Регистры PC и стек нативных методов — служебные области, свои у каждого потока.
Что устарело
Формулировка «классы хранятся в куче, в области PermGen» относится к Java 7 и старше. В Java 8 PermGen удалён, метаданные классов переехали в Metaspace вне кучи, а ключи -XX:PermSize и -XX:MaxPermSize больше не поддерживаются — вместо них используется -XX:MaxMetaspaceSize.
6. StackOverflowError и OutOfMemoryError
Две классические ошибки памяти напрямую связаны с двумя областями.
StackOverflowError возникает, когда закончилось место в стеке потока. Самая частая причина — рекурсия без условия выхода: каждый вызов добавляет кадр, и стек переполняется.
public class StackOverflowDemo {
static int depth = 0;
static void recurse() {
depth++;
recurse(); // нет условия выхода
}
public static void main(String[] args) {
try {
recurse();
} catch (StackOverflowError e) {
System.out.println("Глубина рекурсии: " + depth);
}
}
} OutOfMemoryError: Java heap space возникает, когда в куче не осталось места для нового объекта, а сборщик мусора не может ничего освободить — например, из-за коллекции, которая бесконечно растёт и удерживает ссылки на объекты (утечка памяти).
Размеры областей задаются при запуске JVM:
java -Xms256m -Xmx1g -Xss512k -XX:MaxMetaspaceSize=256m MyApp Здесь -Xms — начальный размер кучи, -Xmx — максимальный, -Xss — размер стека одного потока, -XX:MaxMetaspaceSize — предел Metaspace.
Часто задаваемые вопросы
Где хранятся поля объекта: в стеке или в куче?
Все поля объекта — и ссылочные, и примитивные — хранятся внутри самого объекта, то есть в куче. В стеке оказываются только локальные переменные метода и ссылки на объекты.
Где хранятся статические поля и константы?
Метаданные класса (структура, байт-код методов) находятся в Metaspace, а значения статических полей хранятся в объекте java.lang.Class, который лежит в куче. Объекты, на которые ссылаются статические поля, тоже находятся в куче и не собираются мусорщиком, пока класс загружен.
Java передаёт объекты по ссылке?
Нет. Java всегда передаёт аргументы по значению. Для ссылочного типа копируется значение ссылки, поэтому метод может изменить поля объекта в куче, но не может заставить переменную вызывающего кода указывать на другой объект.
Всегда ли объект создаётся в куче?
С точки зрения модели памяти Java — да. Но JIT-компилятор умеет делать escape analysis: если объект не покидает метод, он может быть разложен на отдельные поля и размещён в стеке или регистрах. Это оптимизация времени выполнения, на исходный код она не влияет.
Почему стек потокобезопасен, а куча нет?
У каждого потока собственный стек, поэтому его локальные переменные недоступны другим потокам. Куча одна на всю JVM, и один и тот же объект могут одновременно менять несколько потоков — отсюда необходимость в synchronized, volatile и других средствах синхронизации.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии