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

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

Об'єкт стає кандидатом на видалення, коли до нього не можна дістатися жодним з GC Root.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Об’єкт стає кандидатом на видалення, коли до нього не можна дістатися жодним з GC Root.

GC Roots — точки входу, від яких GC починає обхід: локальні змінні у стеку, статичні поля, активні потоки, JNI-посилання. Якщо об’єкт недосяжний з GC Roots — він сміття, навіть якщо на нього є посилання від інших недосяжних об’єктів («острови ізоляції»).

Просте правило: Немає шляху від GC Root → об’єкт сміття → GC видалить.

Приклад:

public void example() {
    String s = new String("hello");  // Об'єкт створено
    s = null;                        // Посилання прибрано → об'єкт став сміттям!

    String s2 = new String("world"); // Новий об'єкт
    // Коли метод завершиться → s2 видалиться зі Stack
    // → об'єкт "world" більше недоступний → сміття!
}

GC Roots — точки відліку:

  • Локальні змінні в Stack
  • Статичні поля класів
  • Активні потоки

Якщо до об’єкта не можна дістатися від GC Roots → він сміття.


🟡 Middle Level

Reachability Analysis

Java використовує обхід графу від GC Roots, а не підрахунок посилань (як Python):

GC Roots:
├── Stack змінні (локальні змінні потоків)
├── Статичні поля класів
├── Активні потоки (Thread об'єкти)
├── JNI посилання (з нативного коду)
└── JVM внутрішні об'єкти

Об'єкт живий, якщо:
  Є шлях від будь-якого GC Root → об'єкт

Об'єкт — сміття, якщо:
  Немає шляху від GC Root → об'єкт

Чому не Reference Counting?

// Reference Counting (Python) не працює для циклів:
class Node { Node next; }

Node a = new Node();
Node b = new Node();
a.next = b;  // a → b
b.next = a;  // b → a

a = null;
b = null;

// Reference Counting:
//   a.refCount = 1 (b посилається на a)
//   b.refCount = 1 (a посилається на b)
//   → Ніколи не видаляться! "Island of Isolation"

// Reachability Analysis (Java):
//   Немає шляху від GC Roots → a і b → обидва видаляться! ✅

JIT оптимізація досяжності

public void process() {
    HeavyResource res = new HeavyResource();
    res.doAction();

    // JIT бачить: 'res' більше не використовується
    // → Об'єкт може бути видалений GC прямо зараз!
    // Навіть якщо метод process() ще виконується!

    doSomethingElse();  // res вже може бути видалений
}

Типи посилань (java.lang.ref)

Тип Коли видаляється Застосування
Strong Ніколи (звичайні посилання) Бізнес-об’єкти
Soft При нестачі пам’яті Кеши
Weak При наступному зборі WeakHashMap
Phantom Після фіналізації Очищення ресурсів
// Soft Reference — для кешів
SoftReference<ExpensiveObject> ref = new SoftReference<>(obj);
ExpensiveObject cached = ref.get();  // null, якщо GC видалив

// Weak Reference — для WeakHashMap
WeakReference<User> ref = new WeakReference<>(user);
// GC видалить при наступному зборі, якщо немає Strong посилань

Типові помилки

  1. Забувають прибрати посилання
    // ❌ Об'єкт не видалиться, поки живий список
    static List<Object> cache = new ArrayList<>();
    cache.add(expensiveObject);  // Забули видалити → витік!
    
  2. finalize() — “воскресіння” об’єкта
    // ❌ У finalize() можна присвоїти this статичній змінній
    // → Об'єкт "воскресне"!
    protected void finalize() {
        GlobalCache.add(this);  // Оживлення!
    }
    

    // finalize() викликається один раз. Якщо в ньому присвоїти this статичному // полю — об’єкт знову стане досяжним. Це називається «воскресіння». // У Java 9+ finalize() deprecated — використовуйте Cleaner.


🔴 Senior Level

GC Roots: повна класифікація

GC Roots включають:
├── Stack Locals (локальні змінні активних методів)
├── Stack Parameters (параметри методів у стеку)
├── Static Fields (статичні поля завантажених класів)
├── JNI Global References (глобальні посилання з нативного коду)
├── JNI Local References (локальні посилання в JNI методах)
├── Thread Objects (самі об'єкти Thread — поки не завершені)
├── Monitor Used (об'єкти в synchronized/wait)
├── System Dictionary (завантажені класи)
├── JVM Internal (преалоковані винятки, ClassLoader-и)
└── Unreachable Finalizer Queue (об'єкти в черзі фіналізації)

JIT Reachability: reachabilityFence

// Проблема: об'єкт може бути видалений під час роботи нативного коду
public void nativeAction() {
    NativeResource res = new NativeResource();
    res.doNativeOperation();  // Нативна операція (тривала)

    // JIT бачить: 'res' не використовується після виклику
    // → Може видалити res ДО завершення нативної операції!
    // → Нативний код впаде!
}

// Рішення: reachabilityFence (Java 9+)
public void nativeAction() {
    NativeResource res = new NativeResource();
    res.doNativeOperation();
    Reference.reachabilityFence(res);  // Гарантія: res живий до сюди!
}

Reference API Deep Dive

// Soft Reference — формула очищення
// JVM очищає, коли:
//   час_простою < вільна_пам'ять × SoftRefLRUPolicyMSPerMB
// За замовчуванням: 1000 мс на кожен МБ вільного хіпу

// На великих хіпах (32 ГБ):
//   32 ГБ × 1000 мс/МБ = 32,000 секунд = 9 годин!
//   → SoftReference може жити годинами після "смерті"

// Налаштування: -XX:SoftRefLRUPolicyMSPerMB=100
//   → Очищення через 100 мс/МБ → агресивніше

// Weak Reference — очищається при НАСТУПНОМУ зборі
// → Не чекає нестачі пам'яті!

// Phantom Reference — get() завжди повертає null
// → Використовується ТІЛЬКИ для відстеження видалення
// → Замінює finalize()

Reference Handler Thread

JVM має високопріоритетний потік — Reference Handler

Коли GC виявляє зміну досяжності:
  1. Об'єкт поміщається в Pending List
  2. Reference Handler вилучає з Pending List
  3. Поміщає в ReferenceQueue (вказану при створенні)
  4. Додаток опитує чергу → виконує логіку

→ Асинхронний механізм, не блокує GC

Cleaner (Java 9+)

// Заміна finalize()
public class NativeResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final NativeHandle handle;

    public NativeResource() {
        this.handle = nativeAllocate();
        // Лямбда НЕ має посилання на this → немає "воскресіння"!
        this.cleanable = CLEANER.register(this, () -> {
            nativeFree(handle);  // Очищення нативної пам'яті
        });
    }

    @Override
    public void close() {
        cleanable.clean();  // Ручне очищення
    }
}

// При GC: якщо немає Strong посилань → Cleaner викликає лямбду
// При close(): ручне очищення без очікування GC

Production Experience

Реальний сценарій: JIT видалив об’єкт занадто рано

  • JNI бібліотека: нативна операція 100 мс
  • Java об’єкт видалений GC після виклику нативного методу
  • Нативний код звертається до звільненої пам’яті → SEGFAULT
  • Рішення: reachabilityFence(obj) після нативного виклику

Best Practices

  1. Не покладайтеся на finalize() — використовуйте Cleaner/try-with-resources
  2. SoftReference для кешів, але налаштовуйте SoftRefLRUPolicyMSPerMB Не використовуйте SoftReference як єдиний механізм кешування у production — використовуйте Caffeine/Guava з явними лімітами. SoftReference може викликати excessive GC pressure під навантаженням.
  3. WeakReference для метаданих та реєстрів
  4. reachabilityFence для нативних операцій
  5. Очищуйте посилання у статичних колекціях
  6. Моніторте ReferenceQueue для phantom посилань
  7. Уникайте “воскресіння” об’єктів

Резюме для Senior

  • Reachability Analysis = обхід графу від GC Roots
  • Reference Counting не використовується (проблема циклів)
  • JIT може видалити об’єкт ДО завершення методу
  • reachabilityFence (Java 9+) захищає від передчасного видалення
  • SoftReference = кеши, очищення при нестачі пам’яті
  • WeakReference = метадані, очищення при наступному зборі
  • PhantomReference + Cleaner = заміна finalize()
  • Reference Handler = асинхронний потік для обробки посилань

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

Обов’язково знати:

  • Об’єкт — кандидат на видалення, коли недосяжний з GC Roots (немає жодного шляху від кореня)
  • Java використовує Reachability Analysis (обхід графу), а не Reference Counting — це вирішує проблему «островів ізоляції»
  • JIT може видалити об’єкт ще до завершення методу, якщо бачить, що він більше не використовується
  • 4 типи посилань: Strong (ніколи), Soft (при нестачі пам’яті), Weak (при наступному GC), Phantom (після фіналізації)
  • finalize() deprecated з Java 9 — використовуйте Cleaner або try-with-resources
  • reachabilityFence(obj) гарантує, що об’єкт не буде видалений під час нативної операції
  • Воскресіння об’єкта в finalize() — антипатерн; Cleaner не може воскресити (лямбда без посилання на this)

Часті уточнюючі запитання:

  • Чому Java не використовує Reference Counting? — Не виявляє циклічні посилання (a→b→a), оверхед на кожне присвоювання
  • Що таке «острів ізоляції»? — Група об’єктів, що посилаються один на одного, але недосяжних з GC Roots
  • Коли SoftReference очищується? — При нестачі пам’яті; формула залежить від SoftRefLRUPolicyMSPerMB (за замовчуванням 1000 мс/МБ)
  • Чому PhantomReference.get() завжди повертає null? — Спеціально спроєктований тільки для відстеження факту видалення об’єкта

Червоні прапорці (НЕ говорити):

  • «Об’єкт видаляється, коли всі посилання на нього дорівнюють null» — важлива досяжність з GC Roots, а не null-посилання
  • «finalize() — хороший спосіб звільнити ресурси» — deprecated, непередбачуваний, може воскресити об’єкт
  • «JIT не впливає на час життя об’єктів» — JIT може видалити об’єкт раніше, ніж метод завершиться
  • «GC видаляє об’єкт одразу після obj = null» — видалення відбувається при наступному зборі, якщо об’єкт недосяжний

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

  • [[4. Що таке Garbage Collection]]
  • [[6. Що таке витік пам’яті в Java]]
  • [[25. Що таке GC roots]]
  • [[26. Що таке reachability в контексті GC]]
  • [[27. Чи можна вручну викликати GC]]