Что такое Metaspace (или PermGen)
При проектировании Metaspace инженеры HotSpot разделили данные на две категории:
🟢 Junior Level
Metaspace (Метапространство) — это специализированная область оперативной памяти, выделяемая виртуальной машиной Java для хранения метаданных классов. В ней содержатся: описания классов, байткод методов, сигнатуры полей, аннотации, пул констант (Runtime Constant Pool) и таблицы виртуальных вызовов (vtable и itable).
Эволюция: от PermGen к Metaspace (Java 8)
- До Java 8 (PermGen — Permanent Generation): Метаданные классов находились внутри Java Heap. PermGen имел фиксированный размер (по умолчанию 64–82 МБ). При активной динамической загрузке классов (Spring CGLIB, Hibernate) память мгновенно переполнялась, вызывая постоянный
java.lang.OutOfMemoryError: PermGen space. - Начиная с Java 8 (Metaspace): PermGen был полностью удален. Метаданные классов вынесены за пределы кучи (Off-Heap) в нативную память операционной системы (Native Memory).
Бытовая аналогия
- Heap (Куча): Мебельная фабрика, где стоят собранные столы и стулья (объекты).
- Metaspace: Технический архив фабрики в соседнем здании, где хранятся синьки, чертежи, спецификации ГОСТ и инструкции по сборке (метаданные классов). Чертежи не занимают места на производственной линии, но без них невозможно собрать ни один новый стол.
Сравнительная таблица: PermGen vs Metaspace
| Критерий | PermGen (Java 7 и старше) | Metaspace (Java 8+) |
|---|---|---|
| Где размещается? | Внутри Java Heap (смежная непрерывная память) | В Native Memory ОС (Off-Heap) |
| Ограничение размера | Фиксированное (-XX:MaxPermSize=82m) |
Динамическое (ограничено всей RAM сервера) |
| Где строковый пул? | В PermGen (до Java 7), в Heap (с Java 7) | В Java Heap |
| Где статические поля? | В PermGen | В Java Heap (внутри объекта java.lang.Class) |
| Частота OOM | Очень высокая | Редкая (только при утечках ClassLoader) |
🟡 Middle Level
Куда делись статические поля и String Pool?
При проектировании Metaspace инженеры HotSpot разделили данные на две категории:
- Чистые метаданные JVM (
InstanceKlass,Method, байткод): перенесены в нативный Metaspace. - Java-объекты бизнес-логики: перенесены в управляемую Java Heap.
- Статические поля классов: теперь хранятся как обычные поля внутри зеркального Java-объекта
java.lang.Class, размещенного в куче. - String Constant Pool: интернированные строки вынесены в Heap еще в Java 7, благодаря чему неиспользуемые строки вычищаются стандартным GC без необходимости выгрузки класса.
- Статические поля классов: теперь хранятся как обычные поля внутри зеркального Java-объекта
Compressed Class Space
Если в JVM включены сжатые указатели на классы (-XX:+UseCompressedClassPointers), в 64-битной системе указатель Klass Word в заголовке каждого объекта занимает не 8 байт, а 4 байта.
Для реализации этого сжатия внутри Metaspace выделяется отдельная непрерывная область — Compressed Class Space:
- По умолчанию ее размер составляет 1 ГБ (
-XX:CompressedClassSpaceSize=1g). - Здесь хранятся только C++ структуры
InstanceKlass. Все остальные метаданные методов и аннотаций хранятся в общем пуле Metaspace. - Важно: Переполнение этого 1 ГБ вызовет
OutOfMemoryError: Compressed class space, даже если оперативная память на сервере еще свободна.
Механизм High-Water Mark и Full GC на старте
Параметр -XX:MetaspaceSize задает начальный порог (High-Water Mark), по умолчанию равный всего ~21 МБ:
- При старте Spring Boot приложения идет массовая загрузка классов. Объем метаданных быстро достигает 21 МБ.
- При достижении этой отметки JVM считает, что память исчерпана, и инициирует Full GC с паузой STW, пытаясь выгрузить неиспользуемые ClassLoader-ы!
- Убедившись, что выгружать нечего, JVM поднимает порог выше и продолжает загрузку.
- Через пару секунд память достигает нового порога — следует второй Full GC.
💡 Best Practice для ускорения старта: Устанавливайте начальный размер равным типичному потреблению приложения после прогрева:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m. Это полностью исключит паразитные Full GC на этапе старта сервиса.
🔴 Senior Level
Правила выгрузки классов (ClassLoader Unloading)
Класс в Java не может быть выгружен из Metaspace индивидуально. Классы выгружаются строго пачками вместе со своим загрузчиком (ClassLoader).
Три строгих условия выгрузки ClassLoader:
- 0 живых экземпляров: В куче не должно существовать ни одного живого объекта любого класса, загруженного данным ClassLoader.
- 0 ссылок на
java.lang.Class: В памяти нет ссылок на сами объекты классов этого загрузчика. - Загрузчик недостижим от GC Roots: Сам объект
ClassLoaderдолжен стать недостижимым.
Если хотя бы один сервлет, статический логгер или ThreadLocal держит ссылку на один класс приложения, весь ClassLoader со всеми тысячами сгенерированных прокси застревает в Metaspace навсегда (ClassLoader Leak).
Elastic Metaspace (Java 16+, JEP 387)
До Java 16 аллокатор Metaspace выделял память крупными кусками (Metachunks). При выгрузке классов эти куски оставались фрагментированными в списках свободных блоков (Free Lists) и практически не возвращались операционной системе, раздувая показатель RSS процесса.
В Java 16 реализован Elastic Metaspace:
- Аллокатор переписан на модель Buddy Allocation с гранулярными мелкими блоками.
- Память динамически и агрессивно возвращается операционной системе (
uncommit) при выгрузке неиспользуемых загрузчиков классов. - Фрагментация снижена на 30–50%.
Диагностика Metaspace через jcmd
Для детальной инспекции состояния метаданных на работающей JVM используется утилита:
jcmd <PID> VM.metaspace show-loaders show-classes
Вывод команды показывает:
- Суммарный объем выделенной (
reserved) и реально используемой (committed) нативной памяти. - Список всех активных загрузчиков классов и количество загруженных ими анонимных/динамических классов.
- Степень фрагментации памяти и объем Compressed Class Space.
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Metaspace (Java 8+) — это область нативной оперативной памяти (Off-Heap), где JVM хранит метаданные классов, байткод методов, аннотации и
vtable. Она заменила устаревший PermGen, располагавшийся внутри Heap и вызывавший частые OOM из-за жесткого лимита. Статические поля и строковый пул перенесены в Java Heap, а метаданные классов расширяются динамически. Выгрузка классов из Metaspace возможна только вместе с их ClassLoader, если на него и его классы нет ни одной ссылки. В Docker обязательно нужно задавать-XX:MaxMetaspaceSize, чтобы не допустить убийства пода системным OOM Killer, а начальный-XX:MetaspaceSizeследует поднимать до 256–512 МБ для исключения Full GC на старте».
3–4 каверзных вопроса с ответами
- Почему в Java 8 удалили PermGen и заменили его на Metaspace?
- Ответ: PermGen находился внутри непрерывного адресного пространства Java Heap и имел жестко фиксированный максимальный размер (
-XX:MaxPermSize). В эпоху динамических фреймворков (Spring, Hibernate, Groovy, CGLIB), постоянно генерирующих прокси и классы на лету, PermGen регулярно переполнялся (OutOfMemoryError: PermGen space), требуя ручного подбора параметров. Metaspace вынесен в нативную память ОС, растет динамически и избавлен от ограничений непрерывной кучи.
- Ответ: PermGen находился внутри непрерывного адресного пространства Java Heap и имел жестко фиксированный максимальный размер (
- Где после Java 8 хранятся статические поля классов и строковый пул?
- Ответ: Они хранятся в Java Heap. Статические поля классов размещаются как поля внутри экземпляра
java.lang.Classв куче. Строковый пул (String Constant Pool) был перенесен в Heap еще в Java 7. В Metaspace хранятся строго метаданные JVM (InstanceKlass, методы, байткод, таблицы виртуальных методов).
- Ответ: Они хранятся в Java Heap. Статические поля классов размещаются как поля внутри экземпляра
- Почему заниженное значение
-XX:MetaspaceSizeвызывает серию пауз Full GC при старте приложения?- Ответ: Флаг
-XX:MetaspaceSizeзадает начальный уровень (High-Water Mark), при достижении которого JVM считает необходимым попытаться выгрузить мертвые загрузчики классов и принудительно запускает Full GC. По умолчанию этот порог равен всего ~21 МБ. При старте enterprise-приложения тысячи классов мгновенно пробивают этот порог, провоцируя цепочку тяжелых Full GC подряд.
- Ответ: Флаг
- При каких условиях класс и его метаданные могут быть выгружены из Metaspace?
- Ответ: Отдельный класс выгружен быть не может. Классы выгружаются только целиком вместе со своим загрузчиком (
ClassLoader). Для этого должны одновременно соблюдаться 3 условия: в куче нет ни одного экземпляра классов этого ClassLoader; нет ни одной ссылки на объектыjava.lang.Classэтого загрузчика; сам объектClassLoaderнедостижим от GC Roots.
- Ответ: Отдельный класс выгружен быть не может. Классы выгружаются только целиком вместе со своим загрузчиком (
Типичные ошибки и Red Flags
- 🚩 Утверждение: «Metaspace находится внутри Java Heap» (грубейшая ошибка: Metaspace находится в Native Memory ОС).
- 🚩 Заблуждение: «В Metaspace не может закончиться память» (утечка ClassLoader или отсутствие флага
MaxMetaspaceSizeприведет к падению JVM или OOM Killer). - 🚩 Утверждение: «Статические переменные хранятся в Metaspace» (статические переменные хранятся в куче Heap внутри объектов
Class). - 🚩 Непонимание назначения флага
-XX:MetaspaceSize(считать его минимальным размером памяти вместо порога первого Full GC).
Связанные темы
- Что такое поколения в GC — общая архитектура памяти
- Что такое утечка памяти в Java — утечки ClassLoader
- Какие типы OutOfMemoryError существуют — разбор
Metaspace OOM - Что хранится в Heap — почему статические поля живут в куче
- Что такое heap dump — анализ загрузчиков классов