Привет! На сегодняшнем занятии мы подробно поговорим о «фантомных ссылках» (PhantomReference) в Java. Что это за ссылки такие, почему называются «фантомными» и как ими пользоваться?
Как ты помнишь, в Java есть 4 вида ссылок:
StrongReference (обычные ссылки, которые мы создаем при создании объекта):
Cat cat = new Cat()
cat в этом примере — Strong-ссылка.
SoftReference (мягкая ссылка). У нас была лекция про эти ссылки.
WeakReference (слабая ссылка). Про них тоже была лекция, вот.
PhantomReference (фантомная ссылка).
SoftReference
, WeakReference
и PhantomReference
наследуются от класса Reference
.
Наиболее важные методы при работе с этими классами:
get()
— возвращает объект, на который ссылается эта ссылка;clear()
— удаляет ссылку на объект.
SoftReference
и WeakReference
. Важно помнить, что они работают по-разному с разными видами ссылок.
Мы сегодня не будем подробно рассматривать первые три типа, а поговорим о фантомных ссылках. Остальные виды ссылок мы тоже затронем, но только в той части, где будем говорить, чем фантомные ссылки от них отличаются.
Поехали! :)
Начнем с того, зачем нам вообще нужны фантомные ссылки.
Как ты знаешь, освобождением памяти от ненужных объектов Java занимается сборщик мусора (Garbage Collector или gc).
Сборщик удаляет объект в два «прохода».
В первый проход он только смотрит на объекты, и, если надо, помечает его как «ненужный, подлежащий удалению». Если у этого объекта был переопределен метод finalize()
, он вызывается. Или не вызывается — как повезет. Ты наверняка помнишь, что finalize()
— штука непостоянная :)
Во второй проход сборщика объект удаляется, и память освобождается.
Такое непредсказуемое поведение сборщика мусора создает для нас ряд проблем.
Мы не знаем когда именно начнется работа сборщика мусора. Мы не знаем будет ли вызван метод finalize()
. Плюс ко всему, во время работы finalize()
может быть создана strong-ссылка на объект, и тогда он вообще не будет удален. В системах, требовательных к объему свободной памяти, это может легко привести к OutOfMemoryError
.
Все это подталкивает нас к использованию фантомных ссылок.
Дело в том, что это меняет поведение сборщика мусора. Если на объект остались только фантомные ссылки, то у него:
вызывается метод
finalize()
(если он переопределен);если после работы
finalize()
ничего не изменилось и объект все еще может быть удален, фантомная ссылка на объект помещается в специальную очередь —ReferenceQueue
.
clear()
.
Давай рассмотрим пример. Для начала создадим тестовый класс, который будет хранить в себе какие-то данные.
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()
, чтобы увидеть, что он сработал.
Далее нам понадобится класс, который будет наследоваться от PhantomReference
. Зачем нам нужен такой класс?
Все просто. Так мы сможем добавить дополнительную логику к методу clear()
, чтобы увидеть, что очистка фантомной ссылки действительно произошла (а значит, объект удален).
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class MyPhantomReference<TestClass> extends PhantomReference<TestClass> {
public MyPhantomReference(TestClass obj, ReferenceQueue<TestClass> queue) {
super(obj, queue);
Thread thread = new QueueReadingThread<TestClass>(queue);
thread.start();
}
public void cleanup() {
System.out.println("Очистка фантомной ссылки! Удаление объекта из памяти!");
clear();
}
}
Далее, нам понадобится отдельный поток, который будет ждать, пока сборщик мусора сделает свое дело, и в нашей очереди ReferenceQueue
появятся фантомные ссылки. Как только такая ссылка попадет в очередь, у нее будет вызван метод cleanup()
:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class QueueReadingThread<TestClass> 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();
}
}
И, наконец, нам понадобится метод 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
. Там у нее был вызван метод clear()
(из которого мы сделали cleanup()
, чтобы добавить вывод в консоль).
В итоге объект был удален из памяти.
Теперь ты видишь, как именно это работает :)
Конечно, тебе не нужно зазубривать наизусть всю связанную с фантомными ссылками теорию. Но будет хорошо, если ты будешь помнить хотя бы главные моменты.
Во-первых, это самые слабые ссылки из всех. Они вступают в работу только когда на объект не осталось никаких других ссылок.
Список ссылок, которые мы привели выше, идет по «убыванию силы»:
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
для фантомной.
В-третьих, основная область использование фантомных ссылок -— сложные процедуры удаления объектов из памяти.
Вот и все! :) На этом наше сегодняшнее занятие окончено.
Но на одной теории далеко не уедешь, поэтому пора возвращаться к решению задач! :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ