🧠 Розділ 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).

Пов’язані теми