Вітання! На сьогоднішньому занятті ми докладно поговоримо про «фантомні посилання» (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
для фантомної. По-третє , основна область використання фантомних посилань - складні процедури видалення об'єктів з пам'яті. От і все! :) На цьому наше сьогоднішнє заняття закінчено. Але на одній теорії далеко не поїдеш, тому настав час повертатися до вирішення завдань! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ