Привет! На сегодняшнем занятии мы подробно поговорим о «фантомных ссылках» (PhantomReference) в Java. Что это за ссылки такие, почему называются «фантомными» и как ими пользоваться? Как ты помнишь, в Java есть 4 вида ссылок:
  1. StrongReference (обычные ссылки, которые мы создаем при создании объекта):

    Cat cat = new Cat()

    cat в этом примере — Strong-ссылка.

  2. SoftReference (мягкая ссылка). У нас была лекция про эти ссылки.

  3. WeakReference (слабая ссылка). Про них тоже была лекция, вот.

  4. PhantomReference (фантомная ссылка).

Объекты трех последних видов типизированные (например, SoftReference<Integer>, WeakReference<MyClass>). Классы 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 для фантомной. В-третьих, основная область использование фантомных ссылок -— сложные процедуры удаления объектов из памяти. Вот и все! :) На этом наше сегодняшнее занятие окончено. Но на одной теории далеко не уедешь, поэтому пора возвращаться к решению задач! :)