Вопрос 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? — Не detects cyclic references (a→b→a), оверхед на каждый присваивание
  • Что такое «остров изоляции»? — Группа объектов, ссылающихся друг на друга, но недостижимых из GC Roots
  • Когда SoftReference очищается? — При нехватке памяти; формула зависит от SoftRefLRUPolicyMSPerMB (по умолчанию 1000 мс/МБ)
  • Почему PhantomReference.get() всегда возвращает null? — Специально designed только для отслеживания факта удаления объекта

Красные флаги (НЕ говорить):

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

Связанные темы:

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