🧠 Раздел 3 · Вопрос #11

Что такое 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 разделили данные на две категории:

  1. Чистые метаданные JVM (InstanceKlass, Method, байткод): перенесены в нативный Metaspace.
  2. Java-объекты бизнес-логики: перенесены в управляемую Java Heap.
    • Статические поля классов: теперь хранятся как обычные поля внутри зеркального Java-объекта java.lang.Class, размещенного в куче.
    • String Constant Pool: интернированные строки вынесены в Heap еще в Java 7, благодаря чему неиспользуемые строки вычищаются стандартным GC без необходимости выгрузки класса.

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 МБ:

  1. При старте Spring Boot приложения идет массовая загрузка классов. Объем метаданных быстро достигает 21 МБ.
  2. При достижении этой отметки JVM считает, что память исчерпана, и инициирует Full GC с паузой STW, пытаясь выгрузить неиспользуемые ClassLoader-ы!
  3. Убедившись, что выгружать нечего, JVM поднимает порог выше и продолжает загрузку.
  4. Через пару секунд память достигает нового порога — следует второй Full GC.

💡 Best Practice для ускорения старта: Устанавливайте начальный размер равным типичному потреблению приложения после прогрева: -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m. Это полностью исключит паразитные Full GC на этапе старта сервиса.


🔴 Senior Level

Правила выгрузки классов (ClassLoader Unloading)

Класс в Java не может быть выгружен из Metaspace индивидуально. Классы выгружаются строго пачками вместе со своим загрузчиком (ClassLoader).

Три строгих условия выгрузки ClassLoader:

  1. 0 живых экземпляров: В куче не должно существовать ни одного живого объекта любого класса, загруженного данным ClassLoader.
  2. 0 ссылок на java.lang.Class: В памяти нет ссылок на сами объекты классов этого загрузчика.
  3. Загрузчик недостижим от 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 каверзных вопроса с ответами

  1. Почему в Java 8 удалили PermGen и заменили его на Metaspace?
    • Ответ: PermGen находился внутри непрерывного адресного пространства Java Heap и имел жестко фиксированный максимальный размер (-XX:MaxPermSize). В эпоху динамических фреймворков (Spring, Hibernate, Groovy, CGLIB), постоянно генерирующих прокси и классы на лету, PermGen регулярно переполнялся (OutOfMemoryError: PermGen space), требуя ручного подбора параметров. Metaspace вынесен в нативную память ОС, растет динамически и избавлен от ограничений непрерывной кучи.
  2. Где после Java 8 хранятся статические поля классов и строковый пул?
    • Ответ: Они хранятся в Java Heap. Статические поля классов размещаются как поля внутри экземпляра java.lang.Class в куче. Строковый пул (String Constant Pool) был перенесен в Heap еще в Java 7. В Metaspace хранятся строго метаданные JVM (InstanceKlass, методы, байткод, таблицы виртуальных методов).
  3. Почему заниженное значение -XX:MetaspaceSize вызывает серию пауз Full GC при старте приложения?
    • Ответ: Флаг -XX:MetaspaceSize задает начальный уровень (High-Water Mark), при достижении которого JVM считает необходимым попытаться выгрузить мертвые загрузчики классов и принудительно запускает Full GC. По умолчанию этот порог равен всего ~21 МБ. При старте enterprise-приложения тысячи классов мгновенно пробивают этот порог, провоцируя цепочку тяжелых Full GC подряд.
  4. При каких условиях класс и его метаданные могут быть выгружены из 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).

Связанные темы