Що таке 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 — аналіз завантажувачів класів