Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 потока, создающего фрейм.

Каждый фрейм содержит:

  1. Массив локальных переменных — размеры определяются на этапе компиляции.
  2. Стек операндов — максимальная глубина определяется на этапе компиляции.
  3. Ссылку на пул констант времени выполнения класса текущего метода.
// Как фреймы работают на практике
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ПотоковаяСоздание потокаЗавершение потокаДанные нативных методов

Сводка исключений

Все области данных могут порождать ошибки при нехватке памяти. Вот полная сводка:

ОбластьОшибкаПричина
HeapOutOfMemoryErrorНехватка памяти для нового объекта
Method AreaOutOfMemoryErrorНехватка памяти для загрузки класса
Run-Time Constant PoolOutOfMemoryErrorНехватка памяти при создании пула констант
JVM StackStackOverflowErrorСлишком глубокая рекурсия
JVM StackOutOfMemoryErrorНевозможно создать/расширить стек
Native Method StackStackOverflowErrorПереполнение стека нативного метода
Native Method StackOutOfMemoryErrorНевозможно создать/расширить стек

Резюме

Области данных времени выполнения — это фундамент архитектуры JVM. Вот ключевые моменты, которые нужно запомнить:

  • Heap — хранит все объекты и массивы; управляется сборщиком мусора; общая для всех потоков.
  • Method Area — хранит метаданные классов и байт-код; общая для всех потоков; логически часть кучи.
  • Run-Time Constant Pool — поклассовое хранилище литералов и символических ссылок; выделяется из области методов.
  • PC Register — указатель на текущую инструкцию байт-кода; по одному на каждый поток.
  • JVM Stack — хранит фреймы вызовов методов с локальными переменными и стеком операндов; по одному на каждый поток.
  • Native Method Stack — стек для нативных (JNI) методов; по одному на каждый поток.

Спецификация JVM определяет поведение этих областей, но не их реализацию. Конкретная JVM (HotSpot, OpenJ9, GraalVM) может по-разному организовывать память, выбирать алгоритмы GC и стратегии выделения памяти, при условии соблюдения требований спецификации.