🧠 Розділ 3 · Питання #8

Що таке покоління в GC (young, old, metaspace)

Віртуальна машина Java (JVM) розділяє керовану пам'ять на покоління (Generations). В основі цього розділення лежить слабка гіпотеза про покоління (Weak Generational Hypothesis):


🟢 Junior Level

Віртуальна машина Java (JVM) розділяє керовану пам’ять на покоління (Generations). В основі цього розділення лежить слабка гіпотеза про покоління (Weak Generational Hypothesis):

  1. Переважна більшість створюваних об’єктів (до 95–98%) є тимчасовими й «вмирають» майже миттєво після створення.
  2. Об’єкти, що пережили кілька циклів збирання сміття, з високою ймовірністю продовжать жити дуже довго.

Розділення пам’яті дозволяє застосовувати спеціалізовані, швидкі алгоритми для очищення щойно створених об’єктів, не витрачаючи час на постійне сканування довгоживучих даних.


Три ключові області:

Область Де фізично розташована? Що зберігається? Частота збирання 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):

  1. У будь-який момент часу один із Survivor-регіонів порожній (To-space), а другий містить вижилі об’єкти (From-space).
  2. Під час заповнення Eden запускається Minor GC:
    • Живі об’єкти з Eden та активного Survivor копіюються в порожній сусідній Survivor.
    • Усім скопійованим об’єктам інкрементується лічильник віку (Age).
    • Eden та старий Survivor очищаються одним скиданням покажчика пам’яті.
    • Ролі Survivor змінюються місцями.

Просування в Old Generation (Tenuring & Promotion)

Об’єкт просувається (promoted) з Young Generation в Old Generation у трьох випадках:

  1. Досягнення порогу віку (Tenuring Threshold): Об’єкт успішно пережив задану кількість Minor GC (за замовчуванням -XX:MaxTenuringThreshold=15).
  2. Передчасне просування (Premature Promotion): Якщо сумарний обсяг вижилих об’єктів перевищує місткість регіону Survivor, решта об’єктів скидається безпосередньо в Old Gen.
  3. Пряма алокація великих об’єктів (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%):

  1. Наприкінці Minor GC віртуальна машина обчислює розподіл вижилих об’єктів за віком (Age Table):
    • Обсяг об’єктів з віком 1 = $X_1$
    • Обсяг об’єктів з віком 2 = $X_2$
    • …
  2. JVM підсумовує обсяги: $S = X_1 + X_2 + \dots + X_k$.
  3. Щойно накопичена сума $S$ перевищує $50\%$ обсягу регіону Survivor (TargetSurvivorRatio), поріг віку для цього збирання автоматично знижується до $k$!
  4. Усі об’єкти з віком $\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 каверзних запитання з відповідями

  1. Чому в Young Generation використовуються саме два простори Survivor (S0 і S1), а не один?
    • Відповідь: Для повного усунення фрагментації пам’яті методом човникового копіювання (Scavenge). Якби був один Survivor, видалення померлих об’єктів усередині нього залишало б порожнечі («дірки»), що вимагали б складного ущільнення. Наявність двох областей дозволяє завжди мати один Survivor гарантовано порожнім (To-space), куди вижилі об’єкти з Eden та активного Survivor щільно копіюються підряд (bump-the-pointer), після чого початкові області очищаються миттєво в один крок.
  2. Чи є Metaspace частиною Java Heap?
    • Відповідь: Ні, Metaspace не входить до Java Heap. Починаючи з Java 8, Metaspace розташовується в нативній оперативній пам’яті процесу (Off-Heap). У Java Heap зберігаються лише самі екземпляри об’єктів, масиви, рядковий пул та статичні поля класів (усередині об’єктів java.lang.Class).
  3. Чому максимальний поріг просування (MaxTenuringThreshold) обмежений значенням 15?
    • Відповідь: У заголовку об’єкта (Mark Word на 64-бітній JVM) під лічильник віку об’єкта (age bits) апаратно виділено рівно 4 біти. Чотирма бітами можна представити числа від 0 до 15 ($2^4 - 1$). Число 16 фізично неможливо записати в заголовок об’єкта.
  4. Що таке динамічний поріг віку (Dynamic Tenuring Threshold) і параметр TargetSurvivorRatio?
    • Відповідь: JVM не чекає обов’язкового досягнення віку 15. На кожному Minor GC будується таблиця розподілу вижилих об’єктів за віком. Якщо сумарний обсяг об’єктів від віку 1 до $N$ перевищує відсоток TargetSurvivorRatio (за замовчуванням 50% від розміру регіону Survivor), поріг tenuring threshold примусово знижується до $N$. Усі об’єкти старші за $N$ негайно просуваються в Old Generation, щоб уникнути переповнення Survivor.

Типові помилки та 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 з динамічним розширенням).

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