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

Що таке GC roots

Уявіть генеалогічне дерево. Корені дерева — це GC Roots. Збирач сміття починає рух від коренів і за посиланнями спускається до гілок та листя (звичайних об'єктів у купі). Будь-я...


🟢 Junior Level

GC Roots (корені збирача сміття) — це спеціальні базові об’єкти в пам’яті Java, які вважаються апріорі живими та слугують відправною точкою для алгоритму обходу графу досяжності (Reachability Analysis).

Проста аналогія

Уявіть генеалогічне дерево. Корені дерева — це GC Roots. Збирач сміття починає рух від коренів і за посиланнями спускається до гілок та листя (звичайних об’єктів у купі). Будь-який об’єкт, до якого можна простежити неперервний ланцюжок посилань хоча б від одного кореня, визнається живим. Якщо об’єкт ізольований від усіх коренів (навіть якщо він входить до групи циклічних посилань), він вважається сміттям і підлягає утилізації.

[GC Root: Локальна змінна стека]
        │
        ▼
   [Об'єкт A] ──► [Об'єкт B] ──► [Об'єкт C]   <-- УСІ ЖИВІ (досяжні)

   [Об'єкт D] ◄───► [Об'єкт E]                <-- СМІТТЯ (не досяжні від GC Root,
                                                   хоча посилаються один на одного)

Основні категорії GC Roots:

  1. Локальні змінні та параметри методів у стеках активних потоків;
  2. Статичні поля класів, завантажених у JVM;
  3. Активні об’єкти потоків (Thread);
  4. JNI-посилання (глобальні та локальні посилання в нативному C/C++ коді);
  5. Об’єкти-монітори, утримувані в блоках synchronized.

🟡 Middle Level

1. Вичерпна класифікація GC Roots у HotSpot

Внутрішня архітектура HotSpot класифікує корені на кілька суворих категорій:

Категорія GC Root Опис Місце зберігання
Stack Local Локальні змінні та операнди методів, що виконуються в поточних стекових фреймах. Thread Stack
Static Field Посилання, що зберігаються у статичних полях завантажених класів (у дзеркалах java.lang.Class). Java Heap
Active Thread Самі об’єкти живих потоків (java.lang.Thread), зареєстровані в планувальнику JVM. Heap / Thread Table
JNI Global Глобальні посилання, створені нативним кодом через (*env)->NewGlobalRef. JNI Global Table
JNI Local Аргументи та локальні змінні нативних C-функцій (NewLocalRef). C-Stack фрейму JNI
Monitor Used Об’єкти, на яких у цей момент захоплено монітор синхронізації (synchronized). ObjectMonitor list
JVM Internal Системні об’єкти JVM: базові завантажувачі класів (Bootstrap/Platform ClassLoader), таблиця інтернованих рядків (StringTable), винятки OutOfMemoryError. JVM Native Runtime
+-------------------------------------------------------------------------+
|                              МНОЖИНА GC ROOTS                           |
+-------------------------------------------------------------------------+
|  [Стеки потоків]        [Класи та статика]      [Нативні JNI-посилання] |
|  - Frame 1: userRef     - AppConfig.INSTANCE   - NewGlobalRef(callback)|
|  - Frame 2: buffer      - DatabasePool.cache   - JNI local references  |
|                                                                         |
|  [Системні сутності]    [Блокування]                                    |
|  - Thread instances     - Synchronized objects (ObjectMonitor)         |
|  - System ClassLoaders  - JVM interned string pool                     |
+-------------------------------------------------------------------------+

2. Точне збирання сміття (Exact GC) та OopMaps

Стековий фрейм потоку в операційній системі являє собою масив 64-бітних комірок пам’яті. У комірці може лежати як примітивне число long зі значенням 0x7fa281c00, так і реальний покажчик на об’єкт за адресою 0x7fa281c00.

Як JVM розуміє, що перед нею: число чи посилання на об’єкт?

  • У ранніх віртуальних машинах використовувався Conservative GC (консервативний збирач), який вважав будь-яке значення, схоже на валідну адресу в купі, потенційним об’єктом. Це призводило до помилкових утримань пам’яті.
  • HotSpot є Exact (Precise) GC (точним збирачем сміття). JIT-компілятор під час компіляції байткоду генерує спеціальні карти покажчиків — OopMaps (Ordinary Object Pointer Maps).
  • Для кожної інструкції точки зупинки (Safepoint) в OopMap жорстко зафіксовано: які саме регістри процесора та які зміщення на стеку містять посилання на об’єкти, а які — примітивні типи.
  • Завдяки OopMaps фаза сканування коренів (Root Scanning) займає частки мілісекунди, оскільки збирач сміття обходить лише гарантовані посилання.

🔴 Senior Level

1. Масштабованість Root Scanning та Handshakes

Фаза пошуку та обходу коренів традиційно була частиною Stop-The-World паузи. Якщо в застосунку запущено десятки тисяч потоків (наприклад, до переходу на віртуальні потоки) або завантажено мільйони статичних посилань, час сканування стеків (Root Scanning Time) може зрости до десятків мілісекунд:

\[\text{Час паузи STW} \ge \text{Час зупинки потоків (TTSP)} + \text{Root Scanning Time}\]

Сучасна оптимізація: Thread-Local Handshakes (JEP 312, Java 10+)

Замість зупинки всіх потоків одночасно заради сканування стеків:

  • JVM виконує персональні «рукостискання» (Handshakes) з кожним окремим потоком;
  • У ZGC та Shenandoah сканування стека потоку виконується в момент, коли сам потік досягає точки паузи, не зупиняючи глобально всю систему.

2. Витоки пам’яті через JNI Global References

Одним із найнебезпечніших витоків у корпоративних застосунках є витік через JNI:

// Нативний код C/C++ через JNI:
JNIEXPORT void JNICALL Java_com_example_NativeBridge_registerListener(JNIEnv *env, jobject caller, jobject listener) {
    // Створення глобального GC Root!
    jobject globalListener = (*env)->NewGlobalRef(env, listener);
    // Якщо розробник забув викликати (*env)->DeleteGlobalRef(env, globalListener),
    // об'єкт listener назавжди стає безсмертним GC Root.
}
  • В Eclipse MAT такі об’єкти ідентифікуються в дереві коренів як JNI Global.
  • Небезпека полягає в тому, що витік ініціюється на рівні C/C++, де відсутні засоби автоматичного керування пам’яттю, а Java-код не має змоги скинути це посилання.

3. Де зберігаються статичні поля: міф про Metaspace

[!IMPORTANT] Часта омана на співбесідах: «Статичні змінні зберігаються в Metaspace». Реальність HotSpot: Починаючи з Java 7 (і остаточно закріплено в Java 8 із видаленням PermGen), усі статичні змінні класів зберігаються в Java Heap усередині дзеркального об’єкта java.lang.Class. У Metaspace зберігаються лише нативні метадані структури Klass (таблиці методів, анотації, байткод). Сам об’єкт Class лежить у купі, і його поля є повноцінними коренями GC Roots.


🎯 Шпаргалка для інтерв’ю

30-секундна відповідь для співбесіди

«GC Roots — це базова множина об’єктів, що гарантовано вважаються живими, від яких збирач сміття починає трасування графу досяжності об’єктів у купі. До числа GC Roots входять: локальні змінні та операнди на стеках активних потоків, статичні поля завантажених класів, самі об’єкти живих потоків, глобальні та локальні посилання JNI, а також об’єкти, на яких захоплені блокування synchronized. HotSpot є точним збирачем сміття і за допомогою згенерованих компілятором карт покажчиків (OopMaps) миттєво відрізняє на стеку посилання на об’єкти від примітивних чисел».


Питання підвищеної складності з відповідями

  1. Де фізично розташовуються статичні змінні класів і чому вони є GC Roots?
    • Відповідь: У сучасних версіях Java (починаючи з Java 7/8) статичні змінні фізично розташовуються у звичайній купі (Java Heap) всередині об’єкта-дзеркала java.lang.Class. Доки завантажувач класів (ClassLoader), що завантажив цей клас, залишається живим, сам об’єкт Class досяжний, а його статичні поля виступають як активні GC Roots, утримуючи весь граф дочірніх об’єктів.
  2. Як HotSpot JVM визначає, чи є 64-бітне значення на стеку посиланням чи примітивним типом long?
    • Відповідь: HotSpot є точним збирачем сміття (Exact GC). Під час JIT-компіляції для кожної безпечної точки зупинки (Safepoint) генеруються структури OopMaps (Ordinary Object Pointer Maps). В OopMap для кожної машинної адреси вказано, які регістри процесора та які комірки стека в цей момент містять посилання на об’єкти. При зупинці потоку GC звертається до OopMap і читає лише валідні посилання, не витрачаючи час на здогадки та сканування примітивів.
  3. Чи буде видалено об’єкт, на якому висить блок synchronized, якщо всі посилання на нього в Java-коді занулили?
    • Відповідь: Ні, об’єкт не буде видалено доти, доки потік не залишить блок synchronized. Об’єкт, чий внутрішній монітор (ObjectMonitor) у цей момент утримується або очікується живим потоком, автоматично реєструється в списку моніторів віртуальної машини й визнається системним коренем Monitor Used GC Root.
  4. Що відбувається з фазою Root Scanning при збільшенні кількості потоків з 200 до 10 000?
    • Відповідь: Фаза сканування коренів лінійно масштабується від кількості активних потоків, оскільки збирачу сміття необхідно обійти стек кожного потоку. Велика кількість потоків збільшує тривалість паузи Stop-the-World. Для розв’язання цієї проблеми в сучасній Java впроваджено механізм Thread-Local Handshakes та легковагі віртуальні потоки (Virtual Threads / Project Loom), чиї стеки зберігаються в купі як звичайні об’єкти й не вимагають сканування окремих стеків ядра ОС.

Типові помилки та Red Flags

  • 🚩 Стверджувати, що GC Roots — це виключно статичні змінні. (Red Flag: ігнорування стеків потоків, моніторів та нативних посилань JNI).
  • 🚩 Вважати, що збирач сміття сканує всю пам’ять наосліп у пошуках будь-яких адрес. (Омана: незнання концепції Exact GC та OopMaps).
  • 🚩 Стверджувати, що статичні змінні лежать у Metaspace. (У Metaspace зберігаються метадані C++ Klass, а статичні поля лежать в об’єкті java.lang.Class у Java Heap).

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