Run-Time Data Areas
Материалы
Управление памятью — одна из ключевых задач виртуальной машины. Чтобы понимать, как JVM хранит объекты, выполняет методы и обрабатывает потоки, нужно разобраться в том, на какие области делится память во время работы программы.
Зачем это знать
Каждый Java-разработчик рано или поздно сталкивается с OutOfMemoryError, StackOverflowError или необходимостью настроить параметры памяти JVM. Чтобы действовать осознанно, нужно понимать, как JVM устроена «изнутри» — какие области памяти существуют, чем они отличаются и за что отвечают.
Спецификация JVM определяет шесть областей данных времени выполнения. Они делятся на две категории:
- Общие (shared) — существуют в единственном экземпляре и доступны всем потокам.
- Потоковые (per-thread) — создаются индивидуально для каждого потока.
┌───────────────────────────────────────────────────────────────┐
│ JVM Process │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Общие области (Shared) │ │
│ │ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Heap │ │ Method Area │ │ Run-Time │ │ │
│ │ │ (Куча) │ │ (Область │ │ Constant │ │ │
│ │ │ │ │ методов) │ │ Pool │ │ │
│ │ └───────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Thread 1 │ │ Thread 2 │ ... │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ PC Register │ │ │ │ PC Register │ │ │
│ │ ├──────────────────┤ │ │ ├──────────────────┤ │ │
│ │ │ JVM Stack │ │ │ │ JVM Stack │ │ │
│ │ ├──────────────────┤ │ │ ├──────────────────┤ │ │
│ │ │ Native Method │ │ │ │ Native Method │ │ │
│ │ │ Stack │ │ │ │ Stack │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
│ └──────────────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Примечание. Спецификация JVM описывает абстрактную машину. Она определяет, какие области данных должны существовать и как себя вести, но не диктует конкретную реализацию — расположение в памяти, алгоритмы сборки мусора или способ выделения памяти остаются на усмотрение разработчика конкретной JVM (HotSpot, OpenJ9, GraalVM и т.д.).
Общие области данных (Shared Data Areas)
Общие области создаются при старте JVM и уничтожаются при её завершении. Они доступны всем потокам одновременно.
Куча (Heap)
Куча — это основная область данных времени выполнения, из которой выделяется память для всех экземпляров классов и массивов.
Куча создаётся при старте виртуальной машины. Память для объектов освобождается автоматической системой управления памятью — сборщиком мусора (garbage collector). Объекты никогда не освобождаются явным образом — программисту не нужно (и невозможно) вручную освобождать память, как это делается, например, в C/C++.
// Каждый new выделяет память в куче
Object obj = new Object(); // экземпляр класса — в куче
int[] array = new int[1000]; // массив — в куче
String str = "Hello"; // объект String — в куче
List<String> list = new ArrayList<>(); // ArrayList и его элементы — в куче
Ключевые свойства кучи, определяемые спецификацией:
- Куча может быть фиксированного размера или динамически расширяться/сжиматься.
- Память кучи не обязана быть непрерывной — она может состоять из разрозненных блоков.
- Спецификация не фиксирует алгоритм сборки мусора — реализация выбирает его сама.
Совет. На практике (HotSpot JVM) куча делится на поколения (Young / Old), а размер управляется флагами:
-Xms(начальный размер),-Xmx(максимальный размер). Подробнее об этом — в разделе про GC.
Исключительная ситуация: если для нового объекта нельзя выделить достаточно памяти, JVM выбрасывает OutOfMemoryError.
// Пример: заполнение кучи до исчерпания памяти
List<byte[]> list = new ArrayList<>();
try {
while (true) {
list.add(new byte[1024 * 1024]); // 1 MB за итерацию
}
} catch (OutOfMemoryError e) {
System.err.println("Куча переполнена: " + e.getMessage());
}
Область методов (Method Area)
Область методов — общая область данных, которая хранит поклассовые структуры: константы, информацию о полях и методах, байт-код методов и конструкторов, а также пул констант времени выполнения.
Область методов создаётся при старте виртуальной машины. Она логически является частью кучи, но конкретная реализация JVM может не подвергать её сборке мусора или компрессии.
┌───────────────────────────────────────────────────┐
│ Method Area │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Информация о классе com.example.MyClass: │ │
│ │ - поля (имена, типы, модификаторы) │ │
│ │ - методы (имена, сигнатуры, байт-код) │ │
│ │ - модификаторы класса │ │
│ │ │ │
│ │ Run-Time Constant Pool для MyClass: │ │
│ │ - числовые литералы │ │
│ │ - строковые литералы │ │
│ │ - символические ссылки на методы/поля │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Информация о классе java.lang.String: │ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
Аналогия из мира традиционных языков: область методов похожа на сегмент текста (text segment) процесса в операционной системе — там, где хранится скомпилированный код.
Ключевые свойства:
- Может быть фиксированного размера или динамически расширяться/сжиматься.
- Память не обязана быть непрерывной.
- Спецификация не диктует расположение области методов или политику управления скомпилированным кодом.
Предупреждение. В HotSpot JVM до Java 8 область методов реализовывалась как PermGen (Permanent Generation). Начиная с Java 8 она заменена на Metaspace, которая использует нативную память операционной системы, а не память кучи. Это важно помнить при миграции между версиями Java и при настройке JVM.
Исключительная ситуация: если в области методов не хватает памяти для загрузки нового класса, JVM выбрасывает OutOfMemoryError.
Пул констант времени выполнения (Run-Time Constant Pool)
Пул констант времени выполнения — это поклассовое (или поинтерфейсное) представление таблицы constant_pool из .class-файла.
Он содержит различные виды констант — от числовых литералов, известных на этапе компиляции, до символических ссылок на методы и поля, которые должны быть разрешены во время выполнения. По сути, пул констант выполняет функцию, аналогичную таблице символов в традиционных языках программирования, но содержит более широкий набор данных.
class Example {
static final int MAX = 100; // числовой литерал → constant pool
static final String GREETING = "Hi"; // строковый литерал → constant pool
void process() {
// Символическая ссылка на метод println — будет разрешена в runtime
System.out.println(GREETING);
}
}
Каждый пул констант выделяется из области методов JVM. Пул констант для класса или интерфейса конструируется в момент создания этого класса или интерфейса.
Исключительная ситуация: если при создании класса для пула констант требуется больше памяти, чем доступно в области методов, JVM выбрасывает OutOfMemoryError.
Примечание. Пул констант — это не просто хранилище литералов. Он также содержит символические ссылки на другие классы, поля и методы. Разрешение (resolution) этих ссылок происходит во время выполнения — это часть процесса динамического связывания (dynamic linking), благодаря которому изменения в одном классе реже ломают код другого.
Потоковые области данных (Per-Thread Data Areas)
JVM поддерживает множество потоков выполнения одновременно. Каждый поток имеет три собственных области данных, которые создаются при создании потока и уничтожаются при его завершении. Эти области недоступны другим потокам.
Регистр PC (PC Register)
Каждый поток JVM имеет собственный регистр PC (Program Counter — счётчик команд). В любой момент времени каждый поток выполняет код ровно одного метода — так называемого текущего метода для этого потока.
Поведение регистра PC зависит от типа текущего метода:
| Тип метода | Значение PC-регистра |
|---|---|
Обычный (не native) | Адрес текущей выполняемой инструкции JVM |
native | Не определено (undefined) |
┌─────────────────── Поток ───────────────────┐
│ │
│ PC Register: 0x0042A3F0 │
│ │ │
│ ▼ │
│ Байт-код метода process(): │
│ 0: aload_0 │
│ 1: invokevirtual #5 ← PC указывает сюда │
│ 4: istore_1 │
│ 5: return │
│ │
└─────────────────────────────────────────────┘
Регистр PC достаточно велик, чтобы хранить значение типа returnAddress или нативный указатель на конкретной платформе.
Примечание. Понятие PC-регистра пришло из аппаратной архитектуры. В реальных процессорах program counter указывает на следующую инструкцию для выполнения. В JVM он работает аналогично, но указывает на текущую инструкцию байт-кода, а не машинного кода.
Стек JVM (Java Virtual Machine Stacks)
Каждый поток имеет приватный стек JVM, который создаётся одновременно с потоком. Стек JVM хранит фреймы (frames) — структуры данных, содержащие информацию о вызове метода.
Стек JVM аналогичен стеку в традиционных языках (например, C): он хранит локальные переменные и промежуточные результаты вычислений, а также участвует в вызове методов и возврате из них.
┌─────────────────── JVM Stack ───────────────────┐
│ │
│ ┌───────────────────────┐ ← вершина стека │
│ │ Фрейм метода add() │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Local Variables: │ │ │
│ │ │ [0] this │ │ │
│ │ │ [1] a (int) │ │ │
│ │ │ [2] b (int) │ │ │
│ │ ├──────────────────┤ │ │
│ │ │ Operand Stack: │ │ │
│ │ │ [top] → result │ │ │
│ │ └──────────────────┘ │ │
│ ├───────────────────────┤ │
│ │ Фрейм метода main() │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Local Variables: │ │ │
│ │ │ [0] args │ │ │
│ │ │ [1] result │ │ │
│ │ └──────────────────┘ │ │
│ └───────────────────────┘ ← основание стека │
│ │
└─────────────────────────────────────────────────┘
Ключевые свойства стека JVM:
- Поскольку стек JVM никогда не манипулируется напрямую (кроме как для помещения и извлечения фреймов), фреймы могут размещаться в куче.
- Память стека не обязана быть непрерывной.
- Стек может быть фиксированного размера или динамически расширяться/сжиматься. Если стек фиксированного размера, размер каждого стека можно выбирать независимо при его создании.
Совет. Размер стека можно настроить через флаг
-Xss. Например,-Xss512kустанавливает размер стека в 512 КБ для каждого потока. По умолчанию размер зависит от платформы (обычно 512 КБ — 1 МБ).
Исключительные ситуации, связанные со стеком JVM:
| Ситуация | Исключение |
|---|---|
| Вычисление требует стек большего размера, чем разрешено | StackOverflowError |
| Невозможно выделить память при динамическом расширении стека или при создании стека для нового потока | OutOfMemoryError |
// Пример StackOverflowError — бесконечная рекурсия
void infiniteRecursion() {
infiniteRecursion(); // каждый вызов создаёт новый фрейм на стеке
}
// При переполнении стека получим:
// Exception in thread "main" java.lang.StackOverflowError
// at MyClass.infiniteRecursion(MyClass.java:10)
// at MyClass.infiniteRecursion(MyClass.java:10)
// at MyClass.infiniteRecursion(MyClass.java:10)
// ...
Стек нативных методов (Native Method Stacks)
Реализация JVM может использовать традиционные стеки (в разговорной речи — «C-стеки») для поддержки нативных методов — методов, написанных на языке, отличном от Java (обычно C/C++ через JNI). Стеки нативных методов также могут использоваться самим интерпретатором байт-кода JVM, если он написан на C.
Ключевые свойства:
- Если реализация JVM не поддерживает загрузку нативных методов и сама не использует традиционные стеки, она может не предоставлять стеки нативных методов вообще.
- Стек нативных методов может быть фиксированного размера или динамически расширяться/сжиматься.
- Стеки нативных методов обычно выделяются для каждого потока при его создании.
Примечание. Стек нативных методов очень похож на стек JVM, но предназначен исключительно для нативных методов. Когда Java-код вызывает нативный метод через JNI, выполнение переключается из стека JVM в стек нативных методов, а при возврате — обратно.
// Пример: нативный метод использует Native Method Stack
class NativeExample {
// Объявление нативного метода (реализация на C/C++)
private native int computeHash(byte[] data);
static {
System.loadLibrary("hashlib"); // загрузка нативной библиотеки
}
}
Исключительные ситуации:
| Ситуация | Исключение |
|---|---|
| Вычисление требует стек нативных методов большего размера, чем разрешено | StackOverflowError |
| Невозможно выделить память при расширении или создании стека | OutOfMemoryError |
Фреймы (Frames) — важное дополнение
Хотя фреймы не являются самостоятельной областью данных, они тесно связаны со стеком JVM, и их понимание необходимо для полноты картины.
Фрейм — это структура данных, которая создаётся при каждом вызове метода и уничтожается при завершении метода (как нормальном, так и аварийном). Фреймы выделяются из стека JVM потока, создающего фрейм.
Каждый фрейм содержит:
- Массив локальных переменных — размеры определяются на этапе компиляции.
- Стек операндов — максимальная глубина определяется на этапе компиляции.
- Ссылку на пул констант времени выполнения класса текущего метода.
// Как фреймы работают на практике
static int add(int a, int b) {
int sum = a + b; // Local Variables: [0]=a, [1]=b, [2]=sum
return sum; // Operand Stack: push a, push b, iadd, istore_2
}
public static void main(String[] args) {
int result = add(3, 5);
System.out.println(result);
}
// Выполнение main():
// Фрейм main: locals=[args, result]
// → вызов add(3, 5)
// Фрейм add: locals=[3, 5, 0] → locals=[3, 5, 8]
// ← возврат 8
// Фрейм main: locals=[args, 8]
// → вызов println(8)
// Фрейм println: ...
// ← возврат
Важно: В любой момент времени в данном потоке активен только один фрейм — фрейм выполняемого метода. Он называется текущим фреймом (current frame), а его метод — текущим методом (current method). Фрейм, созданный потоком, локален для этого потока, и ссылаться на него из других потоков нельзя.
Сводная таблица
| Область данных | Общая / Потоковая | Создание | Уничтожение | Содержимое |
|---|---|---|---|---|
| Heap (Куча) | Общая | Старт JVM | Завершение JVM | Экземпляры классов, массивы |
| Method Area (Область методов) | Общая | Старт JVM | Завершение JVM | Данные классов, байт-код методов, пул констант |
| Run-Time Constant Pool | Общая (внутри Method Area) | Загрузка класса | Выгрузка класса | Литералы, символические ссылки |
| PC Register | Потоковая | Создание потока | Завершение потока | Адрес текущей инструкции |
| JVM Stack | Потоковая | Создание потока | Завершение потока | Фреймы (локальные переменные, стек операндов) |
| Native Method Stack | Потоковая | Создание потока | Завершение потока | Данные нативных методов |
Сводка исключений
Все области данных могут порождать ошибки при нехватке памяти. Вот полная сводка:
| Область | Ошибка | Причина |
|---|---|---|
| Heap | OutOfMemoryError | Нехватка памяти для нового объекта |
| Method Area | OutOfMemoryError | Нехватка памяти для загрузки класса |
| Run-Time Constant Pool | OutOfMemoryError | Нехватка памяти при создании пула констант |
| JVM Stack | StackOverflowError | Слишком глубокая рекурсия |
| JVM Stack | OutOfMemoryError | Невозможно создать/расширить стек |
| Native Method Stack | StackOverflowError | Переполнение стека нативного метода |
| Native Method Stack | OutOfMemoryError | Невозможно создать/расширить стек |
Резюме
Области данных времени выполнения — это фундамент архитектуры JVM. Вот ключевые моменты, которые нужно запомнить:
- Heap — хранит все объекты и массивы; управляется сборщиком мусора; общая для всех потоков.
- Method Area — хранит метаданные классов и байт-код; общая для всех потоков; логически часть кучи.
- Run-Time Constant Pool — поклассовое хранилище литералов и символических ссылок; выделяется из области методов.
- PC Register — указатель на текущую инструкцию байт-кода; по одному на каждый поток.
- JVM Stack — хранит фреймы вызовов методов с локальными переменными и стеком операндов; по одному на каждый поток.
- Native Method Stack — стек для нативных (JNI) методов; по одному на каждый поток.
Спецификация JVM определяет поведение этих областей, но не их реализацию. Конкретная JVM (HotSpot, OpenJ9, GraalVM) может по-разному организовывать память, выбирать алгоритмы GC и стратегии выделения памяти, при условии соблюдения требований спецификации.