Що таке reachability в контексті GC
У Java визначено 5 офіційних рівнів досяжності об'єктів (пакет java.lang.ref та специфікація JLS §12.6.1):
🟢 Junior Level
Reachability (досяжність) — це статус життєвого циклу об’єкта в пам’яті Java, що визначає, чи має застосунок можливість отримати доступ до об’єкта через активні посилання від коренів збирача сміття (GC Roots).
У Java визначено 5 офіційних рівнів досяжності об’єктів (пакет java.lang.ref та специфікація JLS §12.6.1):
- Strongly Reachable (Сильно досяжний): До об’єкта можна дістатися звичайним ланцюжком посилань від GC Root без використання об’єктів-обгорток
Reference. Це стандартні посилання в бізнес-коді (User user = new User()). Об’єкт ніколи не видаляється GC. - Softly Reachable (М’яко досяжний): Сильних посилань немає, але є шлях через
SoftReference. Об’єкт зберігається в пам’яті доти, доки JVM не зіткнеться з нестачею купи перед ризикомOutOfMemoryError. - Weakly Reachable (Слабко досяжний): Немає сильних і м’яких посилань, але об’єкт доступний через
WeakReference. Буде гарантовано видалений під час найближчого збирання сміття. - Phantom Reachable (Фантомно досяжний): Об’єкт завершив свій життєвий цикл (фіналізований або очищений), але на нього зареєстровано
PhantomReference. Методget()завжди повертаєnull. Слугує для надійного звільнення нативних ресурсів. - Unreachable (Недосяжний): До об’єкта немає жодних шляхів. Пам’ять об’єкта звільняється фізично.
🟡 Middle Level
1. Порівняльна характеристика типів посилань
| Тип посилання | Клас | Доступ до об’єкта (.get()) |
Коли видаляється збирачем сміття | Основне застосування |
|---|---|---|---|---|
| Strong | Звичайне посилання | obj |
Доки є активне посилання від коренів | Бізнес-логіка, доменні моделі |
| Soft | SoftReference<T> |
Повертає об’єкт або null |
При критичній нестачі пам’яті (перед OOME) | Чутливі до пам’яті кеші |
| Weak | WeakReference<T> |
Повертає об’єкт або null |
При будь-якому циклі збирання сміття (Minor/Major) | WeakHashMap, кеші метаданих |
| Phantom | PhantomReference<T> |
Завжди null |
Після звільнення об’єкта збирачем | Очищення DirectMemory, Cleaner API |
// 1. Звичайне сильне посилання
Order order = new Order(101);
// 2. М'яке посилання (для кешу)
SoftReference<Order> softOrder = new SoftReference<>(order);
// 3. Слабке посилання (для асоціативних метаданих)
WeakReference<Order> weakOrder = new WeakReference<>(order);
// 4. Фантомне посилання (вимагає ReferenceQueue)
ReferenceQueue<Order> queue = new ReferenceQueue<>();
PhantomReference<Order> phantomOrder = new PhantomReference<>(order, queue);
// Розриваємо сильне посилання:
order = null;
2. Черга посилань (ReferenceQueue) та потік Reference Handler
Коли Garbage Collector визначає, що досяжність об’єкта змінилася:
- GC очищає поле
referentусередині об’єкта посилання (для Soft та Weak); - JVM поміщає саме посилання
Referenceу внутрішній списокpending; - Системний високопріоритетний потік віртуальної машини
Reference Handlerперекладає посилання із системного списку до зареєстрованої користувацької чергиReferenceQueue; - Фонові потоки застосунку опитують чергу (
queue.poll()), виконуючи очищення супутніх ресурсів без блокування GC.
[GC виявив зміну статусу]
│
▼
[jdk.internal.ref.Reference.pending list]
│
▼ (потік Reference Handler, priority = 10)
[Користувацька ReferenceQueue]
│
▼ (опитування застосунком)
[Звільнення нативної пам'яті / сокетів]
3. Сучасна альтернатива: java.lang.ref.Cleaner (Java 9+)
Починаючи з Java 9, метод Object.finalize() оголошено застарілим (deprecated), а в Java 18+ готується до повного видалення. Офіційною безпечною заміною є клас Cleaner, побудований на базі фантомних посилань:
public class NativeResourceHolder implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final long nativeHandle;
private final Cleaner.Cleanable cleanable;
public NativeResourceHolder() {
this.nativeHandle = allocateNativeMemory(1024 * 1024);
// Реєстрація дії очищення.
// ⚠️ ВАЖЛИВО: клас State НЕ повинен містити посилань на зовнішній NativeResourceHolder!
this.cleanable = CLEANER.register(this, new CleaningAction(nativeHandle));
}
private static class CleaningAction implements Runnable {
private final long handle;
CleaningAction(long handle) { this.handle = handle; }
@Override
public void run() {
freeNativeMemory(handle);
}
}
@Override
public void close() {
cleanable.clean(); // Ручне очищення, якщо викликано close()
}
}
🔴 Senior Level
1. Формула виживання SoftReference в HotSpot
SoftReference очищаються не випадковим чином. HotSpot JVM розраховує час життя м’якого посилання на основі часу останнього звернення та обсягу вільної пам’яті:
- За замовчуванням прапорець
-XX:SoftRefLRUPolicyMSPerMB=1000(1 секунда на кожен мегабайт вільної пам’яті в купі). - Якщо в купі вільно 10 ГБ пам’яті, м’яке посилання буде утримуватися: \(10240\text{ МБ} \times 1000\text{ мс} \approx 10240\text{ секунд (близько 2.8 годин)}\)
- Проблема в Production: При різких сплесках навантаження пам’ять заповнюється швидше, ніж устигають застаріти м’які посилання. Це змушує JVM запускати екстрений Full GC для їхнього примусового видалення, створюючи неприпустимі сплески latency. Тому в High-Load системах м’які посилання для кешування не рекомендуються — краще використовувати explicit-кеші (Caffeine) з обмеженим розміром (Bounded Cache).
2. Витік пам’яті в WeakHashMap: пастка сильних зв’язків
Клас java.util.WeakHashMap використовує WeakReference для ключів, але зберігає значення за сильними посиланнями:
// ❌ ПРИХОВАНИЙ ВИТІК ПАМ'ЯТІ:
WeakHashMap<Key, Value> map = new WeakHashMap<>();
class Value {
private final Key backReference; // Посилання на ключ!
public Value(Key key) { this.backReference = key; }
}
Key key = new Key("session_1");
map.put(key, new Value(key));
key = null; // Сильне посилання ззовні видалено
Чому WeakHashMap не очиститься?
- Значення
Valueутримується в таблиціEntryсильним посиланням; Valueутримує посилання на об’єктKey;- У результаті на
Keyзамикається сильне посилання через значення:Map -> Entry -> Value -> Key; - Ключ ніколи не стане weakly reachable, і збирач сміття ніколи не видалить цей запис із таблиці!
3. Воскресіння об’єктів (Resurrection) і чому PhantomReference.get() == null
У застарілому механізмі finalize() об’єкт, що став фіналізовним (Finalizer Reachable), міг перехопити посилання на самого себе:
@Override
protected void finalize() {
GlobalRegistry.LEAK.add(this); // Об'єкт воскрес із мертвих!
}
Це породжувало важковловимі баги та повторні цикли GC.
Саме тому творці Java спроєктували PhantomReference.get() так, щоб він завжди повертав null:
- Неможливо отримати доступ до тіла об’єкта, що очищається, через фантомне посилання;
- Об’єкт гарантовано не зможе бути «воскрешений»;
- Фантомне посилання сповіщає застосунок лише про те, що цільовий об’єкт уже помер і фіналізований, дозволяючи безпечно очистити пов’язані нативні дескриптори ОС.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь для співбесіди
«Досяжність (Reachability) у Java визначає статус об’єкта в графі збирача сміття. Виділяють 5 рівнів: Strong (звичайне посилання, захищає від GC), Soft (утримується до виникнення дефіциту купи, розраховується за формулою
SoftRefLRUPolicyMSPerMB), Weak (видаляється під час найближчого GC, основаWeakHashMap), Phantom (методget()завжди повертаєnull, слугує черезReferenceQueueдля звільнення нативних ресурсів на замінуfinalize()), та Unreachable (об’єкт повністю ізольований і видаляється фізично). У сучасній Java для звільнення ресурсів замість небезпечногоfinalize()використовуєтьсяCleaner».
Питання підвищеної складності з відповідями
- Чому
PhantomReference.get()завжди повертаєnull, і навіщо тоді взагалі потрібне це посилання?- Відповідь: Метод
get()навмисно повертаєnull, щоб виключити можливість «воскресіння» об’єкта (Object Resurrection), як це відбувалося при використанніfinalize(), коли об’єкт міг присвоїти посиланняthisстатичному полю.PhantomReferenceреєструється разом ізReferenceQueue. Коли GC збирає об’єкт, посилання потрапляє в чергу. Це слугує чистим сигналом для застосунку: об’єкт гарантовано знищено, і тепер можна безпечно звільнити пов’язані з ним зовнішні нативні ресурси (дескриптори файлів, сокети, виділену пам’ять Cmalloc).
- Відповідь: Метод
- Як
WeakHashMapможе призвести до витоку пам’яті, якщо ключі обгорнуті уWeakReference?- Відповідь: Ключі у
WeakHashMapдійсно утримуються слабко. Однак значення (Value) у мапі утримуються сильними посиланнями. Якщо об’єкт значення прямо чи опосередковано зберігає сильне посилання на свій власний ключ (наприклад,Valueмістить поле з об’єктомKey), формується ланцюжок сильних посилань:Map -> Entry -> Value -> Key. У цьому випадку ключ ніколи не стає weakly reachable, збирач сміття не може його зібрати, і запис ніколи не видаляється з мапи.
- Відповідь: Ключі у
- Чому використання
SoftReferenceяк кешу в High-Load сервісах вважається антипатерном?- Відповідь: Очищення м’яких посилань у HotSpot ініціюється при наближенні купи до вичерпання. За замовчуванням час життя посилання залежить від обсягу вільної купи. Під раптовим високим навантаженням купа швидко заповнюється, що призводить до серії тривалих пауз Stop-The-World Full GC для масової інвалідації soft-посилань. Це викликає різку деградацію latency (GC thrashing). Замість
SoftReferenceна проді використовують детерміновані bounded-кеші (наприклад, Caffeine або Guava Cache) з явними політиками витіснення за розміром і часом (W-TinyLFU / LRU).
- Відповідь: Очищення м’яких посилань у HotSpot ініціюється при наближенні купи до вичерпання. За замовчуванням час життя посилання залежить від обсягу вільної купи. Під раптовим високим навантаженням купа швидко заповнюється, що призводить до серії тривалих пауз Stop-The-World Full GC для масової інвалідації soft-посилань. Це викликає різку деградацію latency (GC thrashing). Замість
- У чому різниця між етапами очищення WeakReference та PhantomReference?
- Відповідь:
WeakReferenceпотрапляє вReferenceQueueі його поле referent зануляється до того, як об’єкт буде фіналізований і видалений з пам’яті (на етапі виявлення слабкої досяжності).PhantomReferenceпотрапляє вReferenceQueueтільки після того, як об’єкт був повністю фіналізований (або якщо у нього немає фіналізатора), гарантуючи, що жодних маніпуляцій з об’єктом усередині JVM відбутися вже не може.
- Відповідь:
Типові помилки та Red Flags
- 🚩 Стверджувати, що
SoftReferenceочищається одразу, як тільки на об’єкт зникли сильні посилання. (Red Flag: нерозуміння різниці між Weak та Soft; SoftReference утримується до дефіциту пам’яті). - 🚩 Думати, що
PhantomReferenceдозволяє викликати методи об’єкта перед його видаленням. (Red Flag: метод.get()завжди повертаєnull). - 🚩 Рекомендувати перевизначати
finalize()для закриття мережевих з’єднань або файлів. (Метод deprecated, не гарантує своєчасного виконання, перевантажує ReferenceHandler потік і може підвісити збирання сміття).