Когда объект становится кандидатом на удаление 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? — Не 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]]