Привет! На сегодняшнем занятии мы подробно поговорим о «фантомных ссылках» (PhantomReference) в Java. Что это за ссылки такие, почему называются «фантомными» и как ими пользоваться?
Если коротко:
Подробное сравнение всех четырех видов с примерами есть в отдельной статье.
Классы
PhantomReference это самая слабая ссылка в Java. Достать через нее объект нельзя, get() всегда возвращает null. Нужна она для другого: поймать момент, когда на объект не осталось никаких других ссылок и его память вот-вот освободят. В этот момент сборщик мусора кладет фантомную ссылку в очередь ReferenceQueue, а ваш код читает очередь и запускает свою процедуру очистки.Кратко
PhantomReferenceэто самая слабая из четырех видов ссылок. Она вступает в игру только тогда, когда на объект не осталось ни обычных, ни мягких, ни слабых ссылок.- Метод
get()у фантомной ссылки всегда возвращаетnull. Это сделано намеренно: объект, который вот-вот удалят, нельзя вернуть к жизни через ссылку на него. - Фантомная ссылка имеет смысл только в паре с очередью
ReferenceQueue: конструктор принимает очередь вторым аргументом. Создать ссылку без очереди можно, но в очередь она никогда не попадет. - Задача фантомной ссылки это уведомление. Сборщик мусора кладет ее в очередь, ваш код читает очередь и по этому сигналу запускает свою процедуру очистки.
- От
PhantomReferenceобычно наследуются. Раз сам объект из ссылки уже не достать, все нужное для очистки складывают в поля класса-наследника. - Типичная область применения это аккуратное освобождение ресурсов вместо ненадежного
finalize(). Готовый инструмент поверх того же механизма это классCleaner, он появился в Java 9.
Четыре вида ссылок в Java
Как ты помнишь, в Java есть 4 вида ссылок:StrongReference (обычные ссылки, которые мы создаем при создании объекта):
Cat cat = new Cat()cat в этом примере это Strong-ссылка.
SoftReference (мягкая ссылка). У нас была лекция про эти ссылки.
WeakReference (слабая ссылка). Про них тоже была лекция, вот.
PhantomReference (фантомная ссылка).
Чем эти четыре вида отличаются друг от друга
Разница между ними в одном: насколько рано сборщик мусора готов забрать объект, на который больше никто, кроме такой ссылки, не смотрит.| Вид ссылки | Что возвращает get() |
Когда сборщик очищает ссылку | Зачем применяют |
|---|---|---|---|
StrongReference, обычная ссылка |
отдельного метода нет, объект доступен напрямую | никогда: пока жива хотя бы одна такая ссылка, объект остается в памяти | обычная работа с объектами |
SoftReference |
объект, пока ссылка не очищена | по усмотрению сборщика, когда памяти начинает не хватать; перед OutOfMemoryError очищаются все мягкие ссылки |
кеши, которые не жалко потерять |
WeakReference |
объект, пока ссылка не очищена | при первой же сборке, как только на объект не осталось обычных и мягких ссылок | служебные словари, например WeakHashMap |
PhantomReference |
всегда null |
когда на объект не осталось ссылок всех трех предыдущих видов | сигнал о том, что пора освобождать связанные с объектом ресурсы |
SoftReference, WeakReference и PhantomReference наследуются от класса Reference.
Наиболее важные методы при работе с этими классами:
get()возвращает объект, на который ссылается эта ссылка;clear()очищает ссылку на объект.
SoftReference и WeakReference. Важно помнить, что они работают по-разному с разными видами ссылок.
Мы сегодня не будем подробно рассматривать первые три типа, а поговорим о фантомных ссылках. Остальные виды ссылок мы тоже затронем, но только в той части, где будем говорить, чем фантомные ссылки от них отличаются.
Поехали! :)
Зачем нужны фантомные ссылки
Начнем с того, зачем нам вообще нужны фантомные ссылки. Как ты знаешь, освобождением памяти от ненужных объектов Java занимается сборщик мусора (Garbage Collector или gc). Сборщик удаляет объект в два «прохода». В первый проход он только смотрит на объекты и, если надо, помечает их как «ненужные, подлежащие удалению». Если у объекта был переопределен методfinalize(), он вызывается. Или не вызывается, как повезет. Ты наверняка помнишь, что finalize() это штука непостоянная :)
Во второй проход сборщика объект удаляется, и память освобождается.
Чем плоха непредсказуемость сборщика
Такое непредсказуемое поведение сборщика мусора создает для нас ряд проблем. Мы не знаем, когда именно начнется работа сборщика мусора. Мы не знаем, будет ли вызван методfinalize(). Плюс ко всему, во время работы finalize() может быть создана strong-ссылка на объект, и тогда он вообще не будет удален. В системах, требовательных к объему свободной памяти, это может легко привести к OutOfMemoryError.
Все это подталкивает нас к использованию фантомных ссылок.
Дело в том, что фантомная ссылка дает то, чего не дает finalize(): надежный сигнал. Если на объект не осталось ничего, кроме фантомных ссылок, то происходит следующее:
вызывается метод
finalize()(если он переопределен);если после работы
finalize()ничего не изменилось и объект все еще может быть удален, сборщик очищает фантомную ссылку и кладет ее в специальную очередьReferenceQueue.
ReferenceQueue уже очищенной, и память из-под объекта освобождается сама, разрешения от вас сборщик не ждет. Ваша задача не «придержать» объект, а успеть сделать то, что положено сделать после его смерти: закрыть файл, вернуть системе нативную память, снять блокировку.
Тут нужна оговорка, потому что старые статьи и учебники пишут прямо противоположное. До Java 9 фантомная ссылка действительно была особенной: сборщик ставил ее в очередь, но не очищал, и пока она лежала там неочищенной, объект оставался в памяти. Удалялся он только после явного вызова clear(). В Java 9 это поведение убрали (запрос JDK-8071507, «Clear phantom reference as soft and weak references do»), и фантомные ссылки стали очищаться автоматически, как мягкие и слабые. В документации сейчас так и написано: сборщик «атомарно очищает все фантомные ссылки на объект», а потом ставит в очередь «эти только что очищенные ссылки».
Вызывать clear() самому больше не нужно: вреда от него нет, но и пользы тоже. В примере ниже он остался только потому, что на него удобно повесить вывод в консоль и увидеть момент срабатывания.
Пример: фантомная ссылка в работе
Давай рассмотрим пример.Шаг 1. Класс, который занимает много памяти
Для начала создадим тестовый класс, который будет хранить в себе какие-то данные.
public class TestClass {
private StringBuffer data;
public TestClass() {
this.data = new StringBuffer();
for (long i = 0; i < 50000000; i++) {
this.data.append('x');
}
}
@Override
protected void finalize() {
System.out.println("У объекта TestClass вызван метод finalize!!!");
}
}
Мы специально как следует «загружаем» объекты данными при создании (добавляем в каждый объект по 50 миллионов символов «х»), чтобы занять побольше памяти.
Кроме того, мы специально переопределяем метод finalize(), чтобы увидеть, что он сработал.
И сразу оговорка про сам finalize(): в рабочем коде его писать нельзя. Метод помечен как @Deprecated(since = "9", forRemoval = true), то есть его собираются из языка удалить, и документация прямым текстом советует вместо него Cleaner и PhantomReference. Здесь он оставлен только как наглядный маячок: по строчке в консоли видно, что сборщик добрался до объекта. Сама финализация пока включена по умолчанию (выключается ключом --finalization=disabled), так что пример запускается и на свежей Java.
Шаг 2. Свой наследник PhantomReference
Далее нам понадобится класс, который будет наследоваться отPhantomReference. Зачем нам нужен такой класс?
Все просто. Так мы сможем добавить дополнительную логику к методу clear(), чтобы увидеть, что фантомная ссылка действительно дошла до очереди (а значит, сборщик уже добрался до объекта).
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class MyPhantomReference extends PhantomReference<TestClass> {
public MyPhantomReference(TestClass obj, ReferenceQueue<TestClass> queue) {
super(obj, queue);
Thread thread = new QueueReadingThread(queue);
thread.start();
}
public void cleanup() {
System.out.println("Очистка фантомной ссылки! Удаление объекта из памяти!");
clear();
}
}
Шаг 3. Поток, который читает очередь
Далее, нам понадобится отдельный поток, который будет ждать, пока сборщик мусора сделает свое дело, и в нашей очередиReferenceQueue появятся фантомные ссылки. Как только такая ссылка попадет в очередь, у нее будет вызван метод cleanup():
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class QueueReadingThread extends Thread {
private ReferenceQueue<TestClass> referenceQueue;
public QueueReadingThread(ReferenceQueue<TestClass> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
System.out.println("Поток, отслеживающий очередь, стартовал!");
Reference ref = null;
//ждем, пока в очереди появятся ссылки
while ((ref = referenceQueue.poll()) == null) {
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
throw new RuntimeException("Поток " + getName() + " был прерван!");
}
}
//как только в очереди появилась фантомная ссылка - очистить ее
((MyPhantomReference) ref).cleanup();
}
}
Шаг 4. Метод main и вызов сборщика
И, наконец, нам понадобится методmain(): вынесем его в отдельный класс Main.
В нем мы создадим объект TestClass, фантомную ссылку на него и очередь для фантомных ссылок. После этого мы вызовем сборщик мусора и посмотрим, что будет :)
import java.lang.ref.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10000);
ReferenceQueue<TestClass> queue = new ReferenceQueue<>();
Reference ref = new MyPhantomReference(new TestClass(), queue);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Вызывается сборка мусора!");
System.gc();
Thread.sleep(300);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Вызывается сборка мусора!");
System.gc();
}
}
Вывод в консоль:
ref = MyPhantomReference@4554617c
Поток, отслеживающий очередь, стартовал!
Вызывается сборка мусора!
У объекта TestClass вызван метод finalize!!!
ref = MyPhantomReference@4554617c
Вызывается сборка мусора!
Очистка фантомной ссылки! Удаление объекта из памяти!
Что мы увидели в выводе
Что же мы здесь видим? Все произошло, как мы и планировали! У нашего класса объекта был переопределен методfinalize(), и он был вызван во время работы сборщика.
Далее сборщик очистил фантомную ссылку и положил ее в очередь ReferenceQueue. Наш поток забрал ее оттуда и вызвал cleanup(), а внутри него clear() (свою обертку мы сделали только ради вывода в консоль).
Память из-под объекта к этому моменту уже освобождается. Последняя строка в консоли это не команда «удалить», а отметка о том, что удаление состоялось и пора прибирать за объектом все остальное.
Теперь ты видишь, как именно это работает :)
Cleaner: то же самое, но короче
Разбирать механизм по частям полезно, но в реальном проекте его редко собирают руками. С Java 9 в пакетеjava.lang.ref есть класс Cleaner, который делает ровно то, что мы написали выше: заводит очередь, держит фантомные ссылки, поднимает свой поток-демон и вызывает вашу процедуру очистки, когда объект стал фантомно достижимым.
import java.lang.ref.Cleaner;
public class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
//задача очистки не должна ссылаться на сам Resource,
//поэтому класс статический и хранит только то, что нужно закрыть
private static class CleanupTask implements Runnable {
private final String name;
CleanupTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Ресурс " + name + " освобожден!");
}
}
private final Cleaner.Cleanable cleanable;
public Resource(String name) {
this.cleanable = cleaner.register(this, new CleanupTask(name));
}
@Override
public void close() {
cleanable.clean();
}
}
Ловушка тут одна, но серьезная: задача очистки не должна ссылаться на сам объект. Если сделать CleanupTask нестатическим внутренним классом или лямбдой, которая читает поля Resource, она будет держать на него сильную ссылку. Объект тогда никогда не станет фантомно достижимым, и очистка не выполнится вообще никогда.
И еще: Cleaner это подстраховка, а не основной способ. Ресурс лучше закрывать явно, через close() и try-with-resources, а очистку по недостижимости оставить на случай, когда явный вызов забыли.
Что важно запомнить о фантомных ссылках
Конечно, тебе не нужно зазубривать наизусть всю связанную с фантомными ссылками теорию. Но будет хорошо, если ты будешь помнить хотя бы главные моменты. Во-первых, это самые слабые ссылки из всех. Они вступают в работу только когда на объект не осталось никаких других ссылок. Список ссылок, которые мы привели выше, идет по «убыванию силы»:StrongReference -> SoftReference -> WeakReference -> PhantomReference
Фантомная ссылка вступит в бой только когда на наш объект не будет ни Strong, ни Soft, ни Weak ссылок :)
Во-вторых, метод get() для фантомной ссылки всегда возвращает null.
Вот простой пример, где мы создаем три разных типа ссылок для трех разных видов автомобилей:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
Sedan sedan = new Sedan();
HybridAuto hybrid = new HybridAuto();
F1Car f1car = new F1Car();
SoftReference<Sedan> softReference = new SoftReference<>(sedan);
System.out.println(softReference.get());
WeakReference<HybridAuto> weakReference = new WeakReference<>(hybrid);
System.out.println(weakReference.get());
ReferenceQueue<F1Car> referenceQueue = new ReferenceQueue<>();
PhantomReference<F1Car> phantomReference = new PhantomReference<>(f1car, referenceQueue);
System.out.println(phantomReference.get());
}
}
Вывод в консоль:
Sedan@4554617c
HybridAuto@74a14482
null
Метод get() вернул вполне нормальные объекты для мягкой ссылки и слабой ссылки, но вернул null для фантомной.
В-третьих, основная область применения фантомных ссылок это сложные процедуры удаления объектов из памяти.
Вот и все! :) На этом наше сегодняшнее занятие окончено.
Но на одной теории далеко не уедешь, поэтому пора возвращаться к решению задач! :)
Вопросы и ответы
Чем PhantomReference отличается от WeakReference и SoftReference?
Тем, что через нее нельзя получить объект:get() всегда возвращает null. Мягкая и слабая ссылки задуманы как способ подержать объект, пока он не мешает, и при случае забрать его обратно. Фантомная задумана как уведомление: доступа к объекту она не дает, а только сообщает, что он стал недостижим и пора освобождать связанные с ним ресурсы.
Почему get() у фантомной ссылки всегда возвращает null?
Чтобы объект нельзя было вернуть к жизни. Если быget() отдавал ссылку, ее можно было бы сохранить в поле и снова сделать объект достижимым ровно в тот момент, когда сборщик уже решил его удалить. В документации Oracle это сформулировано прямо: чтобы объект, подлежащий удалению, таким и остался, доставать его через фантомную ссылку запрещено.
Можно ли создать фантомную ссылку без ReferenceQueue?
Формально да, конструктор принимаетnull вместо очереди. Практического смысла в этом нет: в очередь такая ссылка никогда не попадет, а get() у нее и так всегда null. Получается объект, от которого нет никакой пользы. Фантомная ссылка работает только в паре с очередью.
Зачем наследоваться от PhantomReference, а не использовать его напрямую?
Затем, что из самой ссылки объект уже не достать. Когда ссылка приходит в очередь, у вас на руках только она, поэтому все данные для очистки (дескриптор файла, адрес нативного буфера, имя временного каталога) должны лежать в полях класса-наследника. В примере выше наследник заодно добавляет свой методcleanup().
Когда фантомная ссылка попадает в ReferenceQueue?
Когда сборщик мусора определяет, что объект стал фантомно достижимым, то есть на него не осталось ни обычных, ни мягких, ни слабых ссылок. После этого сборщик ставит в очередь те фантомные ссылки на объект, которые были зарегистрированы с очередью. Читают очередь двумя методами:poll() проверяет ее и сразу возвращает управление, remove() ждет, пока ссылка появится.
Где фантомные ссылки применяются на практике?
Там, где нужно надежно освободить ресурс, который сама JVM освободить не может: нативную память, файловые дескрипторы, соединения. Своими руками их пишут нечасто: с Java 9 для этого есть классCleaner, построенный на тех же фантомных ссылках и очереди. Но понимать устройство полезно: про фантомные ссылки любят спрашивать на собеседованиях.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ