Stack и Heap в Java
Чем стек отличается от кучи в Java, что хранится в каждой области и как управляется жизненный цикл переменных и объектов.
Во время выполнения JVM разделяет управляемую память на две области с принципиально разными задачами. Стек содержит служебные данные для вызовов методов — один фрейм на вызов, с локальными переменными и примитивными значениями внутри него. Куча хранит все объекты, созданные с помощью new, разделяемые всей программой и освобождаемые сборщиком мусора. Почти каждое непонятное поведение Java — почему метод «не может изменить» ваш int, почему две переменные «видят» одно и то же изменение, почему глубокая рекурсия приводит к сбою — напрямую следует из этого разделения.
В этой главе рассматривается, что хранится в каждой области, как различаются их жизненные циклы, почему правило передачи по значению в Java непосредственно вытекает из этого разделения, особый случай пула строк и что происходит, когда одна из областей исчерпывается. Для более широкого понимания того, как эти области вписываются в среду выполнения, см. Архитектура JVM и Модель памяти Java.
Две области, два жизненных цикла
Стек привязан к потоку и управляется автоматически: при вызове метода JVM помещает фрейм, а когда метод возвращает управление, этот фрейм извлекается и его локальные переменные немедленно исчезают. Куча является общей и управляемой: объекты живут до тех пор, пока на них есть хотя бы одна ссылка, после чего сборщик мусора вправе освободить занятое пространство. Ни один объект в куче не исчезает в момент возврата из метода.
| Характеристика | Стек | Куча |
|---|---|---|
| Хранит | Фреймы: локальные переменные, примитивы, ссылки | Объекты, массивы, поля экземпляров |
| Область видимости | Один на поток | Один, общий для всей JVM |
| Время жизни | Фрейм извлекается при возврате из метода | До недостижимости, затем GC |
| Выделение памяти | Push/pop, очень быстро | new, управляется аллокатором |
| Ограничение | Ограничен (-Xss); переполнение бросает StackOverflowError | Ограничена (-Xmx); исчерпание бросает OutOfMemoryError |
| Освобождение | Автоматическое, детерминированное | Сборщик мусора, недетерминированное |
Что на самом деле где находится
Локальная переменная всегда находится в текущем фрейме стека. То, что она содержит, зависит от её типа. Для примитива фрейм хранит само значение. Для объектного типа фрейм хранит только ссылку — объект, на который она указывает, находится в куче.
void example() {
int count = 5; // the value 5 sits in the frame (stack)
double rate = 0.5; // likewise on the stack
int[] data = new int[3]; // 'data' (a reference) is on the stack,
// the 3-element array is on the heap
Point p = new Point(1, 2); // 'p' is on the stack, the Point is on the heap
} // frame popped: count, rate, data, p all gone;
// the array and Point survive until GCПоля экземпляра являются частью объекта, поэтому они находятся в куче вместе с ним — даже поля примитивного типа. Поле private int balance внутри объекта Account хранится в куче, а не в стеке, потому что оно принадлежит объекту, а не какому-либо отдельному вызову метода.
Java всегда передаёт аргументы по значению
Java копирует аргумент в параметр при каждом вызове. Для примитива копируется значение; для объекта копируется ссылка. В Java нет передачи по ссылке, и это единственное правило (подробнее рассматривается в разделе Параметры методов) объясняет три классических сюрприза:
static void bumpPrimitive(int n) { n++; } // changes the copy only
static void mutate(StringBuilder sb) { sb.append("!"); } // edits shared object
static void rebind(StringBuilder sb) { // points the copy elsewhere
sb = new StringBuilder("new"); // caller's variable unchanged
}bumpPrimitive не может повлиять на вызывающий код: он получил копию числа. mutate может изменить то, что видит вызывающий код, потому что скопированная ссылка по-прежнему указывает на объект вызывающего кода в куче. rebind не может, потому что переназначение параметра лишь перенаправляет локальную копию ссылки, а не переменную вызывающего кода.
Пул строк — особый случай в куче
Строковые литералы интернируются: одинаковые литералы используют один объект в пуле строк, поэтому == (сравнение по идентичности ссылок) возвращает true для них. Запись new String("hi") создаёт отдельный объект в куче, поэтому == возвращает false, даже если символы совпадают. Именно поэтому для сравнения строк используют .equals(), который проверяет содержимое, а не идентичность.
String a = "hi";
String b = "hi";
String c = new String("hi");
a == b; // true — both point at the pooled literal
a == c; // false — c is a distinct heap object
a.equals(c); // true — same charactersКогда области исчерпываются
Каждая область имеет ограничение и собственный режим отказа. Неограниченная рекурсия продолжает добавлять фреймы до исчерпания стека, вызывая StackOverflowError. Выделение объектов быстрее, чем GC успевает их освобождать, исчерпывает кучу, вызывая OutOfMemoryError. Оба являются Error, а не Exception — признаками структурной проблемы (отсутствующий базовый случай, утечка памяти), а не чем-то, что нужно регулярно перехватывать.
static int countDown(int n) {
return countDown(n - 1); // no base case -> StackOverflowError
}Практический пример: наблюдаем за поведением двух областей
Эта программа затрагивает все описанные выше правила за один прогон: примитив, переданный по значению, две ссылки на один объект в куче, мутация, которую видит вызывающий код, переназначение, которое он не видит, пул строк, намеренное переполнение стека и удаление последней ссылки на объект.
Что следует вынести из этого прогона:
scoreостаётся равным10, покаnметода достигает110. Примитив был скопирован в новый фрейм, поэтому никакие действия метода не могут достучаться обратно вmain— это передача по значению для примитивов, наглядно продемонстрированная.a.valueиb.valueоба равны42, иa == bвозвращаетtrue, потому чтоCounter b = aскопировал ссылку, а не объект. Один экземпляр в куче, две переменные стека, указывающие на него — измените через любую из них, и обе «увидят» результат.- После
mutateThroughReference(a)значениеa.valueравно999. Метод получил копию ссылки, но копия по-прежнему указывала на тот же объект в куче, поэтому изменение поля видно вызывающему коду. - После
reassignReference(a)значениеa.valueпо-прежнему999, а не-1. Переназначение параметра лишь перенаправило локальную копию ссылки в методе; переменнаяaвызывающего кода не сдвинулась с места. Мутация объекта работает; переназначение переменной — нет. lit1 == lit2возвращаетtrue, ноlit1 == objвозвращаетfalse, тогда как.equalsвозвращаетtrueв обоих случаях. Литералы из пула используют один объект в куче;new Stringсоздаёт отдельный. Пойманный, но не критичныйStackOverflowErrorи финальныйtemp = nullдемонстрируют ограничения двух областей и то, как объект в куче становится доступным для сборки мусора, когда последняя ссылка на него исчезает.
Практика
Связанные главы
- Архитектура JVM — где стек и куча расположены внутри среды выполнения.
- Модель памяти Java — как память ведёт себя в многопоточной среде.
- Сборка мусора — как недостижимые объекты в куче освобождаются.
- Параметры методов — передача по значению в деталях.
- Ссылки — как переменные указывают на объекты в куче.
- Пул строк — почему одинаковые литералы используют один объект.