🧠 Раздел 3 · Вопрос #26

Что такое 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):

  1. Strongly Reachable (Сильно достижим): До объекта можно добраться по обычной цепочке ссылок от GC Root без использования объектов-обёрток Reference. Это стандартные ссылки в бизнес-коде (User user = new User()). Объект никогда не удаляется GC.
  2. Softly Reachable (Мягко достижим): Сильных ссылок нет, но есть путь через SoftReference. Объект сохраняется в памяти до тех пор, пока JVM не столкнется с нехваткой кучи перед риском OutOfMemoryError.
  3. Weakly Reachable (Слабо достижим): Нет сильных и мягких ссылок, но объект доступен через WeakReference. Будет гарантированно удален при ближайшей сборке мусора.
  4. Phantom Reachable (Фантомно достижим): Объект завершил свой жизненный цикл (финализирован или очищен), но на него зарегистрирован PhantomReference. Метод get() всегда возвращает null. Служит для надежного освобождения нативных ресурсов.
  5. 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 определяет, что достижимость объекта изменилась:

  1. GC очищает поле referent внутри объекта ссылки (для Soft и Weak);
  2. JVM помещает саму ссылку Reference во внутренний список pending;
  3. Системный высокоприоритетный поток виртуальной машины Reference Handler перекладывает ссылки из системного списка в зарегистрированную пользовательскую очередь ReferenceQueue;
  4. Фоновые потоки приложения опрашивают очередь (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 рассчитывает время жизни мягкой ссылки на основе времени последнего обращения и объема свободной памяти:

\[\text{Время жизни (мс)} = \text{FreeHeap (в МБ)} \times \text{-XX:SoftRefLRUPolicyMSPerMB}\]
  • По умолчанию флаг -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 не очистится?

  1. Значение Value удерживается в таблице Entry сильной ссылкой;
  2. Value удерживает ссылку на объект Key;
  3. В результате на Key замыкается сильная ссылка через значение: Map -> Entry -> Value -> Key;
  4. Ключ никогда не станет 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».

Вопросы повышенной сложности с ответами

  1. Почему PhantomReference.get() всегда возвращает null, и зачем тогда вообще нужна эта ссылка? Ответ: Метод get() намеренно возвращает null, чтобы исключить возможность «воскрешения» объекта (Object Resurrection), как это происходило при использовании finalize(), когда объект мог присвоить ссылку this статическому полю. PhantomReference регистрируется вместе с ReferenceQueue. Когда GC собирает объект, ссылка попадает в очередь. Это служит чистым сигналом для приложения: объект гарантированно уничтожен, и теперь можно безопасно освободить связанные с ним внешние нативные ресурсы (дескрипторы файлов, сокеты, выделенную память C malloc).

  2. Как WeakHashMap может привести к утечке памяти, если ключи обёрнуты в WeakReference? Ответ: Ключи в WeakHashMap действительно удерживаются слабо. Однако значения (Value) в мапе удерживаются сильными ссылками. Если объект значения прямо или косвенно хранит сильную ссылку на свой собственный ключ (например, Value содержит поле с объектом Key), формируется цепочка сильных ссылок: Map -> Entry -> Value -> Key. В этом случае ключ никогда не становится weakly reachable, сборщик мусора не может его собрать, и запись никогда не удаляется из мапы.

  3. Почему использование SoftReference в качестве кэша в High-Load сервисах считается антипаттерном? Ответ: Очистка мягких ссылок в HotSpot инициируется при приближении кучи к исчерпанию. По умолчанию время жизни ссылки зависит от объема свободной кучи. Под внезапной высокой нагрузкой куча быстро заполняется, что приводит к серии длительных пауз Stop-The-World Full GC для массовой инвалидации soft-ссылок. Это вызывает резкую деградацию latency (GC thrashing). Вместо SoftReference на проде используют детерминированные bounded-кэши (например, Caffeine или Guava Cache) с явными политиками вытеснения по размеру и времени (W-TinyLFU / LRU).

  4. В чём разница между этапами очистки WeakReference и PhantomReference? Ответ: WeakReference попадает в ReferenceQueue и его поле referent зануляется до того, как объект будет финализирован и удален из памяти (на этапе обнаружения слабой достижимости). PhantomReference попадает в ReferenceQueue только после того, как объект был полностью финализирован (или если у него нет финализатора), гарантируя, что никаких манипуляций с объектом внутри JVM произойти уже не может.

Типичные ошибки и Red Flags

  • ❌ Утверждать, что SoftReference очищается сразу, как только на объект пропали сильные ссылки. (Red Flag: непонимание разницы между Weak и Soft; SoftReference удерживается до дефицита памяти).
  • ❌ Думать, что PhantomReference позволяет вызвать методы объекта перед его удалением. (Red Flag: метод .get() всегда возвращает null).
  • ❌ Рекомендовать переопределять finalize() для закрытия сетевых соединений или файлов. (Метод deprecated, не гарантирует своевременного выполнения, перегружает ReferenceHandler поток и может подвесить сборку мусора).

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