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

Коли об'єкт стає кандидатом на видалення GC

Об'єкт у купі (Heap) стає кандидатом на видалення збирачем сміття (Eligible for Garbage Collection) у той самий момент, коли він стає недосяжним (Unreachable) з жодного коренево...


🟢 Junior Level

Об’єкт у купі (Heap) стає кандидатом на видалення збирачем сміття (Eligible for Garbage Collection) у той самий момент, коли він стає недосяжним (Unreachable) з жодного кореневого джерела посилань (GC Root).


Що таке GC Roots?

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

  1. Локальні змінні та параметри у стекових фреймах усіх активних потоків (Stack Locals).
  2. Статичні змінні завантажених класів (Class Statics у Heap).
  3. Самі виконувані об’єкти потоків (Thread, доки потік не завершив роботу).
  4. JNI-посилання (локальні та глобальні посилання в нативному коді C/C++).

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


Побутова аналогія

  • Повітряна куля на прив’язі: Земля — це GC Roots, канати — це посилання, а куля — об’єкт у Heap. Доки до кулі прив’язаний хоча б один канат, що йде до землі, куля утримується. Щойно останній канат перерізано (або відв’язано від землі), куля відлітає й утилізується.

4 класичні способи втрати досяжності в коді:

public class ReachabilityDemo {
    private static Object staticRef = new Object(); // Живий, доки завантажений клас

    public void scenarios() {
        // 1. Явне занулення посилання
        Object obj1 = new Object();
        obj1 = null; // Об'єкт втратив єдиний корінь -> КАНДИДАТ НА GC

        // 2. Переприсвоєння посилання на інший об'єкт
        Object obj2 = new Object(); // Об'єкт А
        obj2 = new Object();        // Об'єкт А втрачено, obj2 вказує на Об'єкт Б -> А кандидат на GC

        // 3. Вихід за область видимості методу
        createTemporaryObject(); // Локальне посилання вмирає разом зі стековим фреймом

        // 4. Ізольований острів (Island of Isolation)
        Node first = new Node();
        Node second = new Node();
        first.neighbor = second;
        second.neighbor = first;

        first = null;
        second = null; // first та second посилаються один на одного, але недосяжні з коренів -> ОБИДВА кандидати на GC!
    }

    private void createTemporaryObject() {
        User temp = new User("Alice");
    } // temp знищується у Stack -> об'єкт new User() стає кандидатом на GC
}

🟡 Middle Level

Трасування графа (Reachability Analysis) vs Підрахунок посилань

На відміну від таких середовищ, як Python, PHP або COM, віртуальна машина Java не використовує алгоритм Reference Counting (Підрахунок посилань):

  • Слабкість Reference Counting: Не вміє розв’язувати циклічні залежності. Якщо об’єкт А тримає посилання на Б, а Б тримає посилання на А, їхні лічильники посилань дорівнюють 1. У разі обриву зовнішніх зв’язків лічильник не падає в 0, породжуючи неусувний витік пам’яті (Island of Isolation).
  • Сила Reachability Analysis у Java: Алгоритм здійснює пошук у ширину/глибину (BFS/DFS), відштовхуючись суворо від множини GC Roots. Оскільки до циклічного острова $A \leftrightarrow B$ не можна дістатися від жодного живого стека чи статичного поля, вся циклічна група утилізується цілком за один прохід збирача.

Сила досяжності та типи посилань (java.lang.ref)

Ступінь досяжності об’єкта залежить від типу посилання, через яке до нього звертаються:

┌────────────────────────────────────────────────────────────────────────┐
│                        ТИПИ ПОСИЛАНЬ У JAVA                            │
├────────────────────────────────────────────────────────────────────────┤
│ 1. Strong Reference (Сильне посилання)                                 │
│    - Звичайний синтаксис: Object obj = new Object();                   │
│    - Об'єкт ніколи не видаляється, доки доступний за сильним посиланням│
├────────────────────────────────────────────────────────────────────────┤
│ 2. Soft Reference (М'яке посилання)                                    │
│    - SoftReference<Data> soft = new SoftReference<>(data);             │
│    - Видаляється збирачем лише перед OutOfMemoryError за дефіциту RAM. │
├────────────────────────────────────────────────────────────────────────┤
│ 3. Weak Reference (Слабке посилання)                                   │
│    - WeakReference<Meta> weak = new WeakReference<>(meta);             │
│    - Видаляється під час першого ж збирання сміття (основа WeakHashMap)│
├────────────────────────────────────────────────────────────────────────┤
│ 4. Phantom Reference (Фантомне посилання)                              │
│    - PhantomReference<Res> ph = new PhantomReference<>(res, queue);   │
│    - get() ЗАВЖДИ повертає null; слугує для пост-мортем очищення.      │
└────────────────────────────────────────────────────────────────────────┘

Проблема «Воскресіння» об’єктів та крах finalize()

До Java 9 метод protected void finalize() дозволяв об’єкту виконати код перед знищенням. Це породжувало критичну архітектурну вразливість — воскресіння об’єкта (Object Resurrection):

// ❌ АНТИПАТЕРН: Воскресіння об'єкта
public class Zombie {
    public static Zombie ZOMBIE_LAIR;

    @Override
    protected void finalize() {
        // Присвоєння this глобальному статичному посиланню повертає об'єкт у GC Roots!
        ZOMBIE_LAIR = this; 
    }
}

JVM гарантувала, що finalize() викликається не більше одного разу за життя об’єкта. Це призводило до зависання черг фіналізації, витоків пам’яті та блокувань.

  • Статус: finalize() оголошено @Deprecated у Java 9 та термінально застарілим (Terminally Deprecated for Removal) у Java 18 (JEP 421).
  • Сучасна заміна: Використання інтерфейсу AutoCloseable з try-with-resources та утиліти java.lang.ref.Cleaner (де очищувальна лямбда суворо ізольована і не захоплює посилання this).

🔴 Senior Level

JIT-аналіз живучості та раннє збирання сміття (Early Reclamation)

Поширена омана: «Об’єкт живе доти, доки не завершиться блок коду або метод, у якому він оголошений».

На практиці JIT-компілятор (C2) будує граф потоку даних в SSA-формі (Static Single Assignment) і виконує Liveness Analysis (Аналіз живучості):

  • Якщо посилання на об’єкт більше ніколи не читається в коді методу, JIT-компілятор формує карту об’єктів (OopMap), у якій відповідний регістр або стековий слот позначається як «мертвий» (Dead Slot).
  • Об’єкт може бути визнаний кандидатом на збирання сміття і фізично знищений у середині виконання методу, навіть якщо метод продовжує працювати ще кілька хвилин!
public void executeHeavyTask() {
    ExternalResource resource = new ExternalResource();
    long handle = resource.getNativeHandle();

    // ⚠️ НЕБЕЗПЕКА: 'resource' більше ніде не згадується!
    // JIT позначає посилання як неживе. GC може зібрати 'resource' просто ЗАРАЗ,
    // закривши нативний дескриптор, доки довгий нативний виклик ще працює у фоні!
    doLongNativeCall(handle);

    // ✅ ЗАХИСТ: Гарантує збереження досяжності до цієї точки
    Reference.reachabilityFence(resource);
}

Статичний метод Reference.reachabilityFence(Object ref) (доступний з Java 9) створює бар’єр оптимізації компілятора, забороняючи оголошувати посилання мертвим до проходження цієї інструкції.


Архітектура Reference Processing та Reference Handler Thread

Коли GC виявляє, що на об’єкт більше немає сильних посилань, але є Soft, Weak або Phantom:

  1. GC переводить об’єкт у спеціальний внутрішній стан і зв’язує його у список Pending References.
  2. У JVM працює спеціальний потік найвищого пріоритету — Reference Handler.
  3. Цей потік вилучає посилання з черги Pending і поміщає їх в асоційовану користувацьку чергу ReferenceQueue.
  4. Споживач (наприклад, фоновий потік очищення з’єднань у пулі) забирає фантомне посилання з ReferenceQueue і безпечно звільняє пов’язані нативні ресурси (пам’ять поза купою DirectByteBuffer через Cleaner).

Пастка SoftReference на великих Heap (> 32 ГБ)

Час утримання об’єктів за м’якими посиланнями обчислюється JVM за формулою: \(\text{Survival Time (мс)} = \text{Free Heap (МБ)} \times \text{SoftRefLRUPolicyMSPerMB}\) За замовчуванням -XX:SoftRefLRUPolicyMSPerMB=1000 (1 секунда на кожен мегабайт вільної пам’яті).

⚠️ Highload Trap: Якщо на сервері виділено -Xmx64g і 40 ГБ купи вільно, м’яке посилання утримуватиметься в пам’яті: \(40\,000 \text{ МБ} \times 1\,000 \text{ мс} = 40\,000\,000 \text{ мс} \approx 11 \text{ годин!}\) Використання SoftReference як кешу на великих серверах гарантує деградацію продуктивності: кеш не скидається годинами, купа забивається застарілими даними, а GC починає відчувати колосальний тиск під час сканування величезного графа посилань. У production для кешів слід використовувати спеціалізовані бібліотеки (Caffeine) з явними політиками витіснення за розміром та часом (W-TinyLFU).


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

30-секундна відповідь (Elevator Pitch)

«Об’єкт стає кандидатом на збирання сміття в момент, коли він стає недосяжним (Unreachable) від коренів GC Roots (активні стеки потоків, статичні поля, JNI-посилання). Java використовує алгоритм Tracing, завдяки чому легко ізолює й очищає перехресні циклічні посилання (Island of Isolation). Важливо розуміти, що завдяки аналізу живучості JIT-компілятора об’єкт може стати кандидатом на збирання ще до завершення методу, якщо посилання на нього більше не читається (для захисту застосовується Reference.reachabilityFence). Для керування часом життя використовуються типи посилань: Soft (очищення під час дефіциту пам’яті), Weak (очищення під час першого збирання) та Phantom (пост-мортем очищення нативних ресурсів через Cleaner)».


3–4 каверзних запитання з відповідями

  1. Чи може об’єкт бути зібраний GC, доки метод, у якому він створений, ще продовжує виконуватися?
    • Відповідь: Так. Якщо JIT-компілятор у процесі аналізу потоку керування визначає, що посилання на об’єкт більше жодного разу не читається до кінця методу, він виключає його з карти покажчиків (OopMap). Якщо на об’єкт немає інших посилань, GC має повне право зібрати його просто під час роботи методу. Для запобігання передчасному збиранню (наприклад, під час утримання нативних дескрипторів у JNI) наприкінці методу викликають Reference.reachabilityFence(obj).
  2. Що таке «Острів ізоляції» (Island of Isolation) і чому він не небезпечний для Java?
    • Відповідь: Острів ізоляції — це група з декількох об’єктів, які посилаються один на одного (наприклад, кільцевий двозв’язний список), але жоден із них не має вхідного посилання від коренів GC Roots. У системах із підрахунком посилань (Reference Counting) це призвело б до витоку пам’яті. У Java алгоритм трасування починає обхід суворо від GC Roots: оскільки в ізольовану групу не можна зайти з кореня, збирач сміття позначає всю групу як недосяжну і знищує всі об’єкти за один цикл.
  3. Чому PhantomReference.get() завжди повертає null?
    • Відповідь: Метод get() у PhantomReference навмисно завжди повертає null, щоб розробник не міг отримати сильне посилання на об’єкт, який уже визнано фіналізованим і який фактично мертвий. Це унеможливлює «воскресіння» об’єкта, захищаючи інваріанти пам’яті JVM.
  4. Чому SoftReference небезпечні для реалізації кешів на серверах із великою оперативною пам’яттю?
    • Відповідь: За замовчуванням JVM очищає SoftReference за формулою «1 секунда життя на кожен мегабайт вільної купи» (SoftRefLRUPolicyMSPerMB=1000). Якщо на сервері вільно 30 ГБ пам’яті, м’які посилання утримуватимуть об’єкти понад 8 годин, навіть якщо вони давно втратили актуальність. Це перешкоджає звільненню пам’яті, роздуває граф об’єктів і збільшує тривалість пауз на фазі розмітки GC.

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

  • 🚩 Твердження: «Об’єкт видаляється відразу, як тільки ми написали obj = null» (занулення лише робить об’єкт кандидатом; фактичне видалення відбудеться недетерміновано під час чергового циклу GC).
  • 🚩 Використання finalize() у новому коді (метод застарів, не дає гарантій часу виконання і ламає throughput збирача сміття).
  • 🚩 Омана: «Об’єкт гарантовано живе до закриваючої фігурної дужки методу }» (JIT-оптимізація може зробити об’єкт кандидатом на видалення значно раніше).
  • 🚩 Використання SoftReference як єдиної стратегії обмеження кешу в highload-сервісах.

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