Введение
Java 11 — это вторая LTS (Long-Term Support) версия после Java 8, и на сегодняшний день она остаётся одной из самых распространённых в production-средах. Понимание архитектуры JVM в Java 11 критически важно для разработчиков, отвечающих за производительность, стабильность и эффективное использование ресурсов.
В этой статье мы:
- детально разберём структуру памяти JVM в Java 11,
- сравним её с Java 8,
- покажем реальные примеры и типичные проблемы,
- дадим рекомендации по тонкой настройке и диагностике.
1. Обзор архитектуры JVM в Java 11
JVM остаётся виртуальной машиной, исполняющей байт-код, но её внутреннее устройство продолжает эволюционировать. Основные компоненты:
Class Loader Subsystem
Загружает, линкует и инициализирует классы
Runtime Data Areas
Области памяти (о них — основная часть статьи)
Execution Engine
Выполняет байт-код (интерпретатор, JIT-компилятор, GC)
Native Method Interface (JNI)
Взаимодействие с нативным кодом
Native Libraries
Системные библиотеки ОС
Главные изменения в Java 9–11:
- полное исчезновение PermGen (уже в Java 8),
- появление Metaspace как замены,
- модульная система JPMS (Project Jigsaw),
- новые алгоритмы GC: ZGC (экспериментальный в Java 11), Shenandoah (в OpenJDK, но не в Oracle JDK по умолчанию),
- улучшенная работа с off-heap памятью.
2. Области памяти JVM в Java 11
Все области памяти делятся на потоковые (приватные для каждого потока) и общие (разделяемые между потоками).
2.1. Heap (Куча) — общая память
Назначение: хранение всех экземпляров объектов и массивов.
Структура кучи (по умолчанию — Parallel GC):
+-----------------------------+
| Old Generation |
+-----------------------------+
| Young Generation |
| +--------+ +--------+ |
| | Eden | |Survivor| |
| +--------+ +--------+ |
| (S0 / S1) |
+-----------------------------+
🔍 В Java 11 поведение кучи не изменилось по сравнению с Java 8. Но доступны новые GC, которые кардинально меняют её организацию.
Пример:
public class HeapExample {
public void process() {
// Объект создаётся в Eden
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");
// Если sb "переживёт" Minor GC — попадёт в Survivor,
// затем — в Old Gen
}
}
Настройка:
-Xms2g # начальный размер кучи
-Xmx4g # максимальный размер
-XX:NewRatio=3 # соотношение Old:Young = 3:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1 (на каждый Survivor)
2.2. Metaspace — замена PermGen
Где находится: в нативной памяти ОС, а не в куче JVM.
Что хранит:
- метаданные классов (имена, методы, поля),
- статические переменные (сами значения — в куче!),
- константный пул (constant pool),
- JIT-компиляторные структуры.
❗ Важно: в Java 8 Metaspace уже появился, но в Java 11 его управление стало ещё более гибким.
Пример утечки Metaspace:
// Динамическая генерация классов (например, через CGLib, Hibernate, Spring Proxy)
public class MetaspaceLeak {
public static void main(String[] args) throws Exception {
while (true) {
// Каждая итерация создаёт НОВЫЙ класс
// → MetaSpace растёт бесконечно
}
}
}
Диагностика:
# Включить логирование Metaspace
-XX:+PrintGCDetails
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
Настройка:
-XX:MetaspaceSize=128m # триггер для первого GC в Metaspace
-XX:MaxMetaspaceSize=512m # жёсткий лимит
⚠️ Без MaxMetaspaceSize JVM может исчерпать всю виртуальную память ОС → java.lang.OutOfMemoryError: Metaspace.
2.3. Thread Stacks — приватная память
Каждый поток имеет свой стек. Хранит:
- локальные переменные (примитивы и ссылки),
- параметры методов,
- return-адреса,
- stack frames (по одному на вызов метода).
Пример:
public int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // каждый вызов — новый frame
}
При factorial(10_000) → StackOverflowError.
Настройка:
-Xss1m # размер стека на поток (по умолчанию: 1M в 64-бит JVM)
💡 Уменьшайте -Xss, если приложение создаёт тысячи потоков (например, в reactive-системах лучше использовать виртуальные потоки — но они появились только в Java 21).
2.4. PC Register (Program Counter)
- Указывает на адрес следующей инструкции байт-кода.
- У каждого потока — свой.
- Не потребляет значительной памяти.
2.5. Native Method Stack
Для методов, помеченных как native (через JNI). Использует стек ОС.
2.6. Code Cache
Новая важная область! Хотя она существовала и раньше, в Java 11 её роль возросла.
Назначение: хранит нативный код, сгенерированный JIT-компилятором (HotSpot).
- Когда метод "горячий" (часто вызывается), JIT компилирует его в native-код.
- Этот код кэшируется в Code Cache.
Проблема:
Если Code Cache переполняется → JIT отключается, и производительность падает.
Настройка:
-XX:ReservedCodeCacheSize=240m # по умолчанию ~240M
-XX:+PrintCodeCache # логировать использование
3. Garbage Collection в Java 11: что изменилось?
Доступные GC-алгоритмы в Java 11:
Serial GC
Встроен
Для одноядерных систем, маленьких heaps
Parallel GC
По умолчанию
Высокая throughput, но остановки
CMS
Удалён в Java 14, но ещё есть в 11
Low-latency, но deprecated
G1 GC
Рекомендуется для heaps > 4 ГБ
Предсказуемые паузы
ZGC
Экспериментальный(включается флагом)
Паузы < 10 мс, heaps до TB
Shenandoah
В OpenJDK (не в Oracle JDK)
Low-pause, concurrent GC
Включить G1 (часто используется в продакшене):
-XX:+UseG1GC
Включить ZGC (требует -XX:+UnlockExperimentalVMOptions в Java 11):
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
4. Отличия Java 11 от Java 8 (кратко)
PermGen
Есть (java 8)
Полностью удалён (java 11)
Metaspace
Введён (java 8)
Улучшен, стабилен (java 11)
GC по умолчанию
Parallel (java 8)
Parallel (но G1 активно рекомендуется) (java 11)
Новые GC
Нет (java 8)
ZGC (эксперимент), Shenandoah (OpenJDK) (java 11)
Модульность
Нет (java 8)
JPMS (Project Jigsaw) — влияет на ClassLoader (java 11)
JRE/JDK
Раздельные (java 8)
Только JDK (JRE "упакован" внутрь) (java 11)
💡 Ключевое для памяти: структура heap и metaspace осталась той же, но управление — точнее, а инструменты — мощнее.