W3docs

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
}

Практический пример: наблюдаем за поведением двух областей

Эта программа затрагивает все описанные выше правила за один прогон: примитив, переданный по значению, две ссылки на один объект в куче, мутация, которую видит вызывающий код, переназначение, которое он не видит, пул строк, намеренное переполнение стека и удаление последней ссылки на объект.

java— editable, runs on the server

Что следует вынести из этого прогона:

  • 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 демонстрируют ограничения двух областей и то, как объект в куче становится доступным для сборки мусора, когда последняя ссылка на него исчезает.

Практика

Практика
Метод получает параметр типа StringBuilder. Внутри метода вы вызываете sb.append('x'). После возврата из метода StringBuilder вызывающего кода содержит добавленный символ 'x'. Почему?
Метод получает параметр типа StringBuilder. Внутри метода вы вызываете sb.append('x'). После возврата из метода StringBuilder вызывающего кода содержит добавленный символ 'x'. Почему?

Связанные главы

Was this page helpful?