Що таке покоління в GC (young, old, metaspace)
Віртуальна машина Java (JVM) розділяє керовану пам'ять на покоління (Generations). В основі цього розділення лежить слабка гіпотеза про покоління (Weak Generational Hypothesis):
🟢 Junior Level
Віртуальна машина Java (JVM) розділяє керовану пам’ять на покоління (Generations). В основі цього розділення лежить слабка гіпотеза про покоління (Weak Generational Hypothesis):
- Переважна більшість створюваних об’єктів (до 95–98%) є тимчасовими й «вмирають» майже миттєво після створення.
- Об’єкти, що пережили кілька циклів збирання сміття, з високою ймовірністю продовжать жити дуже довго.
Розділення пам’яті дозволяє застосовувати спеціалізовані, швидкі алгоритми для очищення щойно створених об’єктів, не витрачаючи час на постійне сканування довгоживучих даних.
Три ключові області:
| Область | Де фізично розташована? | Що зберігається? | Частота збирання GC |
|---|---|---|---|
| Young Generation (Молоде) | Всередині Java Heap | Усі нові об’єкти застосунку, локальні DTO, тимчасові буфери | Дуже часто (Minor GC, лічені мілісекунди) |
| Old Generation (Старше / Tenured) | Всередині Java Heap | Об’єкти-довгожителі, пули з’єднань, синглтони Spring, кеші | Рідко (Major / Full GC, десятки й сотні мс) |
| Metaspace (Метадані) | Поза купою (у Native Memory ОС) | Описи класів (Klass), байткод методів, Runtime Constant Pool |
Рідко (тільки під час вивантаження ClassLoader) |
Побутова аналогія
- Young Generation: Кошик для сміття під робочим столом. Очищається прибиральником кілька разів на день за пару секунд (зім’яті чернетки).
- Old Generation: Домашня шафа для одягу. Речі лежать роками, генеральне прибирання проводиться рідко, але займає пів дня.
- Metaspace: Технічний паспорт шафи та креслення квартири. Це не самі речі, а будівельна документація, що зберігається в окремій папці на полиці.
Життєвий цикл об’єкта (Promotion Pipeline)
[ new Object() ]
│
▼
┌───────┐
│ Eden │ ──(Minor GC: вижилі копіюються)──► ┌──────────────┐
└───────┘ │ Survivor S0 │
└──────┬───────┘
│ (Minor GC: ping-pong)
▼
┌──────────────┐
│ Survivor S1 │
└──────┬───────┘
│ (Вік >= 15 або переповнення)
▼
┌──────────────┐
│ Old Gen │
└──────────────┘
🟡 Middle Level
Внутрішня будова Young Generation: Eden та Survivor
Young Generation розбите на три регіони:
- Eden (Рай): Сюди потрапляють абсолютно всі нові об’єкти через швидкі локальні буфери потоків TLAB.
- Survivor 0 (S0 / From Space) та Survivor 1 (S1 / To Space): Два буфери однакового розміру для вижилих об’єктів.
За замовчуванням співвідношення розмірів задається прапорцями:
-XX:NewRatio=2: розмір Old Gen удвічі більший за сумарний Young Gen (Young займає $1/3$ купи, Old — $2/3$).-XX:SurvivorRatio=8: розмір Eden відноситься до одного Survivor як $8:1$ (Eden займає 80% Young Gen, S0 — 10%, S1 — 10%).
Алгоритм човникового копіювання (Ping-Pong Copying):
- У будь-який момент часу один із Survivor-регіонів порожній (
To-space), а другий містить вижилі об’єкти (From-space). - Під час заповнення Eden запускається Minor GC:
- Живі об’єкти з Eden та активного Survivor копіюються в порожній сусідній Survivor.
- Усім скопійованим об’єктам інкрементується лічильник віку (Age).
- Eden та старий Survivor очищаються одним скиданням покажчика пам’яті.
- Ролі Survivor змінюються місцями.
Просування в Old Generation (Tenuring & Promotion)
Об’єкт просувається (promoted) з Young Generation в Old Generation у трьох випадках:
- Досягнення порогу віку (Tenuring Threshold): Об’єкт успішно пережив задану кількість Minor GC (за замовчуванням
-XX:MaxTenuringThreshold=15). - Передчасне просування (Premature Promotion): Якщо сумарний обсяг вижилих об’єктів перевищує місткість регіону Survivor, решта об’єктів скидається безпосередньо в Old Gen.
- Пряма алокація великих об’єктів (Humongous Allocation): Дуже великі масиви, що не вміщуються в TLAB/Eden, створюються одразу в Old Generation.
Metaspace: заміна PermGen з Java 8
До Java 8 метадані класів зберігалися в PermGen (Permanent Generation) усередині Java Heap:
- PermGen мав жорстко обмежений розмір (
-XX:MaxPermSize), що під час динамічної генерації класів (Spring CGLIB, Hibernate) викликало регулярнийOutOfMemoryError: PermGen space. - У PermGen також розташовувався рядковий пул (String Pool) та статичні поля.
З Java 8 PermGen повністю видалено:
- Метадані класів перенесені в Metaspace — область нативної пам’яті операційної системи (Off-Heap).
- Metaspace розширюється автоматично до фізичного ліміту оперативної пам’яті сервера (якщо не обмежений прапорцем
-XX:MaxMetaspaceSize). - Статичні змінні класів та String Pool перенесені у звичайний Java Heap.
🔴 Senior Level
Динамічний розрахунок порогу віку (Dynamic Tenuring Threshold)
Прапорець -XX:MaxTenuringThreshold=15 задає лише максимально можливу межу віку. На практиці JVM динамічно перераховує фактичний поріг просування на кожному збиранні Minor GC за допомогою параметра -XX:TargetSurvivorRatio (за замовчуванням 50%):
- Наприкінці Minor GC віртуальна машина обчислює розподіл вижилих об’єктів за віком (Age Table):
- Обсяг об’єктів з віком 1 = $X_1$
- Обсяг об’єктів з віком 2 = $X_2$
- …
- JVM підсумовує обсяги: $S = X_1 + X_2 + \dots + X_k$.
- Щойно накопичена сума $S$ перевищує $50\%$ обсягу регіону Survivor (
TargetSurvivorRatio), поріг віку для цього збирання автоматично знижується до $k$! - Усі об’єкти з віком $\ge k$ негайно виштовхуються в Old Generation, щоб запобігти переповненню Survivor.
Фізичне обмеження віку: 4 біти в Mark Word
Часте запитання на співбесідах Senior: «Чому параметр MaxTenuringThreshold не може бути встановлений більше 15?»
Відповідь:
У заголовку будь-якого об’єкта в JVM HotSpot поле Mark Word має жорстко фіксовану бітову структуру. Під зберігання віку об’єкта виділено рівно 4 біти:
\(2^4 - 1 = 16 - 1 = 15\)
Максимальне число, яке можна закодувати в 4 бітах — $11112 = 15{10}$. Будь-яка спроба виставити -XX:MaxTenuringThreshold=16 завершиться помилкою ініціалізації віртуальної машини.
Еволюція поколінь: Generational ZGC (Java 21+, JEP 439)
Першопочатково ZGC (Java 11–17) був однопоколінним (Single-Generational): збирач сканував усю купу цілком. Це призводило до високого споживання ресурсів CPU на високонавантажених застосунках із високим темпом алокації (Allocation Rate).
У Java 21+ представлено Generational ZGC (-XX:+UseZGC -XX:+ZGenerational):
- Пам’ять розділена на Young та Old покоління, але зберігається безконкурентна природа ZGC (паузи $< 1$ мс).
- Збирання в Young поколінні запускаються набагато частіше, задіюючи мінімум потоків CPU.
- Досягнуто 4-кратного зниження навантаження на процесор порівняно з однопоколінним ZGC при збереженні ультранизьких затримок.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«JVM ділить пам’ять на покоління на основі слабкої гіпотези про покоління: більшість об’єктів помирають молодими. Young Generation (Eden + 2 Survivor S0/S1) зберігає нові об’єкти й очищається швидкими збираннями Minor GC алгоритмом копіювання. Old Generation зберігає вижилі довгоживучі об’єкти (що досягли порогу віку або виштовхнуті при переповненні Survivor) й очищається важчими Major/Full GC. Metaspace розташовується поза купою (у Native Memory) і зберігає метадані класів, байткод та Runtime Constant Pool. Граничний вік об’єкта в Young Gen дорівнює 15, оскільки під лічильник віку в Mark Word заголовка об’єкта виділено рівно 4 біти».
3–4 каверзних запитання з відповідями
- Чому в Young Generation використовуються саме два простори Survivor (S0 і S1), а не один?
- Відповідь: Для повного усунення фрагментації пам’яті методом човникового копіювання (Scavenge). Якби був один Survivor, видалення померлих об’єктів усередині нього залишало б порожнечі («дірки»), що вимагали б складного ущільнення. Наявність двох областей дозволяє завжди мати один Survivor гарантовано порожнім (
To-space), куди вижилі об’єкти з Eden та активного Survivor щільно копіюються підряд (bump-the-pointer), після чого початкові області очищаються миттєво в один крок.
- Відповідь: Для повного усунення фрагментації пам’яті методом човникового копіювання (Scavenge). Якби був один Survivor, видалення померлих об’єктів усередині нього залишало б порожнечі («дірки»), що вимагали б складного ущільнення. Наявність двох областей дозволяє завжди мати один Survivor гарантовано порожнім (
- Чи є Metaspace частиною Java Heap?
- Відповідь: Ні, Metaspace не входить до Java Heap. Починаючи з Java 8, Metaspace розташовується в нативній оперативній пам’яті процесу (Off-Heap). У Java Heap зберігаються лише самі екземпляри об’єктів, масиви, рядковий пул та статичні поля класів (усередині об’єктів
java.lang.Class).
- Відповідь: Ні, Metaspace не входить до Java Heap. Починаючи з Java 8, Metaspace розташовується в нативній оперативній пам’яті процесу (Off-Heap). У Java Heap зберігаються лише самі екземпляри об’єктів, масиви, рядковий пул та статичні поля класів (усередині об’єктів
- Чому максимальний поріг просування (
MaxTenuringThreshold) обмежений значенням 15?- Відповідь: У заголовку об’єкта (Mark Word на 64-бітній JVM) під лічильник віку об’єкта (age bits) апаратно виділено рівно 4 біти. Чотирма бітами можна представити числа від 0 до 15 ($2^4 - 1$). Число 16 фізично неможливо записати в заголовок об’єкта.
- Що таке динамічний поріг віку (Dynamic Tenuring Threshold) і параметр
TargetSurvivorRatio?- Відповідь: JVM не чекає обов’язкового досягнення віку 15. На кожному Minor GC будується таблиця розподілу вижилих об’єктів за віком. Якщо сумарний обсяг об’єктів від віку 1 до $N$ перевищує відсоток
TargetSurvivorRatio(за замовчуванням 50% від розміру регіону Survivor), порігtenuring thresholdпримусово знижується до $N$. Усі об’єкти старші за $N$ негайно просуваються в Old Generation, щоб уникнути переповнення Survivor.
- Відповідь: JVM не чекає обов’язкового досягнення віку 15. На кожному Minor GC будується таблиця розподілу вижилих об’єктів за віком. Якщо сумарний обсяг об’єктів від віку 1 до $N$ перевищує відсоток
Типові помилки та Red Flags
- 🚩 Твердження: «Metaspace розташований усередині купи Java Heap» (груба помилка: Metaspace перебуває в Native Memory).
- 🚩 Омана: «Об’єкти потрапляють в Old Generation тільки після досягнення віку 15» (об’єкти можуть потрапити туди раніше через dynamic tenuring threshold, humongous allocations або нестачу місця в Survivor).
- 🚩 Твердження: «Minor GC очищає всю купу цілком» (Minor GC працює суворо всередині Young Generation; для старшого покоління працюють Concurrent Marking або Full GC).
- 🚩 Плутанина між PermGen та Metaspace (PermGen був усередині Heap із фіксованим розміром; Metaspace — у Native Memory з динамічним розширенням).
Пов’язані теми
- Що таке Young Generation — детальна будова Eden та Survivor
- Що таке Old Generation (Tenured) — алгоритми збирання старшого покоління
- Що таке Metaspace (або PermGen) — архітектура пам’яті класів
- Які алгоритми GC існують — Copying vs Mark-Sweep vs Mark-Compact
- Що таке Garbage Collection — загальні принципи роботи збирача сміття