Коли об'єкт стає кандидатом на видалення GC?
Об'єкт стає кандидатом на видалення, коли до нього не можна дістатися жодним з GC Root.
🟢 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 посилань
Типові помилки
- Забувають прибрати посилання
// ❌ Об'єкт не видалиться, поки живий список static List<Object> cache = new ArrayList<>(); cache.add(expensiveObject); // Забули видалити → витік! - 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
- Не покладайтеся на finalize() — використовуйте Cleaner/try-with-resources
- SoftReference для кешів, але налаштовуйте
SoftRefLRUPolicyMSPerMBНе використовуйте SoftReference як єдиний механізм кешування у production — використовуйте Caffeine/Guava з явними лімітами. SoftReference може викликати excessive GC pressure під навантаженням. - WeakReference для метаданих та реєстрів
- reachabilityFence для нативних операцій
- Очищуйте посилання у статичних колекціях
- Моніторте ReferenceQueue для phantom посилань
- Уникайте “воскресіння” об’єктів
Резюме для 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-resourcesreachabilityFence(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]]