среда, 7 сентября 2011 г.

Использование PhantomReferences в Java


Платформа Java предоставляет несколько типов ссылок для связи между объектами:

  1. Жесткие ссылки (Strong References) - стандартные, известные нам ссылки. Если на объект есть хоть одна жесткая ссылка, то данный объект не будет утилизирован при сборке мусора.

  2. Мягкие ссылки (Soft References) - создаются с помощью вызова new SoftReference<T>(T obj, ReferenceQueue<T> queue) или new SoftReference(T obj). Если на объект есть только мягкая ссылка, то будет выполнена попытка утилизации данного объекта при сборке мусора в случае, если приложению не хватает памяти.

  3. Слабые ссылки (WeakReferences) - создаются с помощью вызова new WeakReference<T>(T obj, ReferenceQueue<T> queue) или new WeakReference<T>(T obj). Если на объект есть только слабая ссылка, то будет выполнена попытка утилизации данного объекта при сборке мусора.

  4. Фантомные ссылки (PhantomReferences) - создаются с помощью вызова new PhantomReference<T>(T obj, ReferenceQueue<T> queue). Если на объект есть только фантомная ссылка, то будет выполнена попытка утилизации данного объекта при сборке мусора. Сам объект при этом не будет удален из памяти до тех пор, пока на него существует фантомная ссылка или данная фантомная ссылка не очищена с помощью вызова метода clear(). Так же стоит заметить, что метод get() фантомной ссылки всегда возвращает null.


Возникает вполне закономерный вопрос: зачем нужны ссылки, которые фактически держат объект в памяти, но по которым к нему нельзя получить доступ?

Проблемы использования метода finalize()


Утилизация объекта сборщиком мусора производится в два этапа:

  1. Выполнение метода finalize() объекта.

  2. Непосредственно освобождение памяти, выделенной под объект.


Оба эти действия выполняются из потока сборщика мусора. При этом поведение сборщика мусора существенно отличается в случаях если у объекта переопределен метод finalize() и если не переопределен. Если у объекта переопределен метод finalize(), то при первом проходе сборщика мусора данный объект помечается как требующий удаления. Затем у него выполняется метод finalize(). Сам объект при этом удаляется только при последующих проходах сборщика мусора.

Поведение объекта, у класса которого не переопределен метод finalize():



Поведение объекта, у класса которого переопределен метод finalize():



Однако при вызове метода finalize() на объект может быть создана жесткая ссылка. Например, реализовали при финализации сериализацию объекта в файл. Передаем объект сериализатору, тот выполнил свою работу, но продолжает держать ссылку на объект. Тогда при последующих за выполнением метода finalize() проходах сборщика мусора объект не сможет быть удален.

Данное поведение может быть критичным в системах, орабатывающих большие объекты в условиях ограниченного объема памяти. Например, в системах обработки больших изображений. Загружать новое изображение мы можем только после того, как из памяти выгрузилось старое. В методе finalize() мы уведомляем систему о выгрузке старого изображения, но на самом деле из-за проблемы, описанной выше, объект изображения не утилизируется. Система этого не знает и пытается загрузить другое - новое - изображение. В итоге существует риск получить OutOfMemoryError.

Поведение PhantomReference


При наличие на объект только фантомной ссылки, сборщик мусора предпринимает следующие действия по утилизации объекта:

  1. Выполняет метод finalize().

  2. Если после выполнения предыдущего шага на объект не было создано жестких ссылок и объект может быть удален из памяти, то фантомная ссылка помещается в очередь ReferenceQueue.


Стоит заметить, что если метод finalize() у объекта переопределен, то эти действия выполняются в разных проходах сборщика мусора.

Как было отмечено во вступлении к статье, непосредственное удаление объекта из памяти не производится до очистки фантомной ссылки. После выполнения данной очистки объект может быть удален при следующем проходе сборщика мусора. Таким образом, если у объекта не переопределен метод finalize(), то для его удаления потребуется в лучшем случае два прохода сборщика мусора, если же переопределен - три прохода.

Использование PhantomReference


Разберемся теперь с тем, как использовать фантомные ссылки. Удобный способ получить уведомление о помещении ссылки в очередь - создать отдельный поток, который будет периодически опрашивать данную очередь, вызывая метод ReferenceQueue#poll(). Данный метод возвращает ссылку в случае ее наличия в очереди или null - в случае отсутствия. Поместить код данного потока и механизмы его запуска удобнее всего в класс-наследник PhantomReference. Так же в данном наследнике можно создать метод, осуществляющий действия по очистке системы после удаления объекта - те действия, которые раньше размещались в методе finalize(). К таким действиям относятся: закрытие соединений, сбрасывание состояния объекта на диск, освобождение сессий и т.д. Стоит отметить, что для выполнения некоторых действий могут понадобиться знания о внутреннем состоянии объекта. Ни в коем случае нельзя сохранять объект как поле в классе-наследнике PhantomReference, т.к. в таком случае будет создана жесткая ссылка на данный объект, т.е. при сборке мусора с ним не будет выполняться никаких действий. Необходимо сохранять в классе-наследнике PhantomReference значения только тех полей объекта, которые будут нужны для проведения процедуры очистки.

Итак, стратегия использования PhantomReference может быть следующей:

  1. Создаем класс-наследник PhantomReference. В данном классе реализуем метод, осуществляющий очистку окружения после удаления объекта.

  2. Создаем класс-поток, в конструктор которого передается очередь ReferenceQueue, связанная с экземпляром созданного ранее класса PhantomReference. В методе run() данного потока реализуем опрос этой очереди. Как только из ReferenceQueue будет считано непустое значение - вызываем метод класса-наследника PhantomReference, осуществляющий очистку окружения.

  3. В конструкторе класса-наследника PhantomReference сохраняем значения нужных в дальнейшем полей переданного объекта после чего запускаем поток, созданный на втором шаге алгоритма.


Описанная стратегия иллюстрируется следующим кодом:

  1. import java.lang.ref.PhantomReference;

  2. import java.lang.ref.Reference;

  3. import java.lang.ref.ReferenceQueue;

  4.  

  5.  

  6. public class RessurectDemo {

  7.    

  8.     private A a;

  9.    

  10.     public static class A {

  11.         private RessurectDemo demo;

  12.         private String data;

  13.        

  14.         public String getData() {

  15.             return data;

  16.         }

  17.        

  18.         public A(RessurectDemo demo) {

  19.             this.demo = demo;

  20.             StringBuffer buff = new StringBuffer();

  21.             for (long i = 0; i < 50000000; i++) {

  22.                 buff.append('a');

  23.             }

  24.             this.data = buff.toString();

  25.         }

  26.  

  27.         @Override

  28.         protected void finalize() throws Throwable {           

  29.             System.out.println("A.finalize()");        

  30.         }              

  31.     }

  32.        

  33.     private static class MyPhantomReference<T> extends PhantomReference<T> {       

  34.        

  35.         public MyPhantomReference(T obj, ReferenceQueue<? super T> queue) {

  36.             super(obj, queue);

  37.             Thread thread = new MyPollingThread<T>(queue);

  38.             thread.start();

  39.         }      

  40.        

  41.         public void cleanup() {

  42.             System.out.println("cleanup()");

  43.            

  44.             // Clear Reference!!!

  45.             clear();           

  46.         }

  47.        

  48.         public static class MyPollingThread<T> extends Thread {

  49.  

  50.             private ReferenceQueue<? super T> referenceQueue;

  51.            

  52.             public MyPollingThread(ReferenceQueue<? super T> referenceQueue) {

  53.                 this.referenceQueue = referenceQueue;

  54.             }

  55.            

  56.             @Override

  57.             public void run() {

  58.                 System.out.println("MyPollingThread started");

  59.                 Reference<?> ref = null;

  60.                 while ((ref = referenceQueue.poll()) == null) {

  61.                     try {

  62.                         Thread.sleep(50);

  63.                     }

  64.                     catch (InterruptedException e) {

  65.                         throw new RuntimeException("Thread " + getName() + " has been interrupted");

  66.                     }

  67.                 }

  68.                

  69.                 if (ref instanceof MyPhantomReference<?>) {

  70.                     ((MyPhantomReference<?>) ref).cleanup();

  71.                 }

  72.             }

  73.         }

  74.     }

  75.    

  76.     public static void main(String[] args) throws InterruptedException {

  77.         RessurectDemo demo = new RessurectDemo();

  78.        

  79.         Thread.sleep(20000);

  80.        

  81.         Reference<A> ref = new MyPhantomReference<RessurectDemo.A>(new A(demo), new ReferenceQueue<RessurectDemo.A>());

  82.                

  83.         System.out.println("ref = " + ref);

  84.         System.out.println("A = " + ref.get());    

  85.                

  86.         Thread.sleep(10000);       

  87.        

  88.         System.out.println("System.gc()");

  89.         System.gc();

  90.        

  91.         Thread.sleep(400);

  92.        

  93.         System.out.println("ref = " + ref);

  94.         System.out.println("A = " + ref.get());    

  95.  

  96.         Thread.sleep(10000);

  97.        

  98.         System.out.println("System.gc()");

  99.         System.gc();       

  100.         Thread.sleep(400);

  101.  

  102.         Thread.sleep(10000);

  103.        

  104.         System.out.println("System.gc()");

  105.         System.gc();   

  106.        

  107.         Thread.sleep(10000);

  108.     }

  109. }

  110.  



Предлагаю в комментариях делиться замечаниями по теме. Возможно у читателей есть свое понимание особенностей использования фантомных ссылок - поделитесь им. Особенно был бы благодарен за приведение реальных примеров использования.

UPD 08.09.2011: Статья немного обновлена по результатам обсуждения в комментариях.

UPD 11.09.2011: Подумалось, что применять PhantomReferences для реализации пулов соединений действительно не получится, а вот для знаменитой задачи про двухуровневый кэш - почему бы нет.

Понравилось сообщение - подпишитесь на блог

26 комментариев:

mvmn комментирует...

"Единственный способ получить уведомление о помещении ссылки в очередь - создать отдельный поток, который будет периодически опрашивать данную очередь"
Но можно ведь сделать наследник-враппер для ReferenceQueue. Или имелось в виду в общем случае, когда мы не контролируем создание нового инстанса ReferenceQueue?

Pavel Samolisov комментирует...

У ReferenceQueue доступны для переопределения только методы, наследуемые от Object, и poll(), remove() и remove(long). К сожалению метод enqueue() недоступен для переопределения.

Я думал переопределить метод enqueue() у Reference, но при помещении ссылки в очередь сборщиком мусора данный метод не вызывается.

mvmn комментирует...

P.S. А, понял, метод boolean enqueue(Reference r) - package private.

mvmn комментирует...

Да, я сразу было недоглядел, извините.

mvmn комментирует...

Дал пост почитать товарищам - сделали пару замечаний:

>> В отличие от WeakReference PhantomReference попадает в очередь только после утилизации объекта сборщиком мусора.

False. PhantomReference javadoc:
* Unlike soft and weak references, phantom references are not
* automatically cleared by the garbage collector as they are enqueued. An
* object that is reachable via phantom references will remain so until all
* such references are cleared or themselves become unreachable.

>> К сожалению, ReferenceQueue не реализует паттерн Observer и не может уведомлять подписчиков о помещении в нее ссылки сборщиком мусора.

"Какой еще Observer? Из какого потока должны вызываться ваши Listener-ы? Если из потока GC, то это будет тот же finalize() с теми же граблями. Главная идея PhantomReference в том и заключается, чтоб контролировать процесс освобождения памяти, в т.ч. контролировать то, в каком потоке это будет происходить"

hkvd комментирует...

Спасибо, отлично проясняет использование PhantomReference.

Pavel Samolisov комментирует...

@mvmn Спасибо за ценные замечания.

По первому пункту - поправил пост. Немного перекомпоновал, чтобы повествование шло более гладко.

По второму. Я согласен, что паттерн Observer здесь лишний. Иначе не стоило бы вообще возиться с очередью, регистрировали бы сразу в Reference некий листенер и все. Но считаю неверным, что PhantomReference позволяют контролировать в каком потоке происходит освобождение памяти. Строго говоря, освобождение памяти ВСЕГДА производится сборщиком мусора в его потоке. По крайней мере в SUN JRE это так. PhantomReference позволяет лишь корректно выполнить все подготовительные процедуры по сохранению состояния объекта, логированию, закрытию соединений и т.д., после чего разрешить освобождение памяти.

0x1e комментирует...

Хотя основная идея передана верно, пример совсем никуда не годится.
Вызов сборщика мусора не гарантируется при вызове System.gc()

Кроме того вы упустили важный момент, а именно _когда_ ссылка гарантированно появится в очереди. Javadoc говорит только что это произойдет _после_ отработки сборщика мусора и _перед_ очисткой памяти занимаемой обьектом.

В реальности время между этими событиями неконтролируемо. В тестах которые я делал, сылка появлялась в очереди лишь тогда, когда старый участок памяти был нужен для новх обьектов. Если машине этот участок не будет нужен, время попадания ссылки в очередь стремится в бесконечность.

Что это значит? Это значит, что освобождение критически важных ограниченных ресурсов вроде сетевых соединений _нельзя_ отдавать на совесть фантомных ссылок.

Этот вывод парадоксальный для меня так как в сети многие предлагают использовать фантомные ссылки для построения пулов соединений, но как я не искал примеры кода таких пулов, я их просто не нашел.

Pavel Samolisov комментирует...

Я знаю, что System.gc() не гарантирует, что будет вызван сборщик мусора сразу же. Но можно поиграться с размерами кучи и длительностью пауз. Когда я запускал данный пример у себя - сборщик мусора корректно отрабатывал после вызова System.gc().

При этом на SUN JRE ссылка четко попадала в очередь сразу же после вызова System.gc() (первого или второго в зависимости от того переопределен ли метод finalize() у объекта, на который создавалась ссылка).

На JRockit ссылка в очередь у меня так ни разу и не попала.

Тот факт, что время между срабатыванием cleanup() и физическим удалением объекта из памяти неконтролируемо действительно навевает грусть и тоску. По сути, описанная мною проблема: загрузка нового большого объекта в то время, когда старый еще не выгружен, с помощью фантомных ссылок тоже не решается.

0x1e комментирует...

на второй вызов System.gc() сановская JVM делает полный gc, это так, но это просто особенность Sun JVM.

На самом деле как раз загрузка больших обьектов в память и вызовет попадание ссылки на старый обьект в очередь - поскольку памяти требуется много, вероятность, что память старого обьекта потребуется практически равна 100%. Так что именно в этом случае все должно получится.

Вы просто обнуляете ссылку на старый обьект, и загружаете новый, без опасений что гдето там ктото восстановит ссылку на старый обьект - и таким образом избежите потенциального OOM, возможного в случае использования finalize().

Кроме того, я забыл сказать спасибо, так как ваша статья - пожалуй самый толковый обзор фантомных ссылок, который попадался мне на глаза, везде ограничиваются слабыми и мягкими ссылками а фантомные тактично игнорируют :)

Pavel Samolisov комментирует...

Вам тоже спасибо за ценные замечания. Возможно коллективными усилиями вас, mvmn и других читателей выработается адекватное и главное полное понимание механизма фантомных ссылок.

Pavel Samolisov комментирует...

З.Ы. А вообще данная статья мне не нравится. Сумбурное изложение и резкие переходы между разделами. Похоже двухмесячная пауза в блогинге сказывается :)

itallix комментирует...

Затрагивая стратегию использования подобных ссылок, хотелось бы отметить класс из библиотеки guava: FinalizablePhantomReference. Он позволяет упростить код - избежать необходимости создавать отдельный поток для опроса очереди. Для очистки окружения достаточно лишь переопределить метод finalizeReferent().

Спасибо за статью!

Pavel Samolisov комментирует...

Вам спасибо.

Tema комментирует...

Никогда даже не пытался понять зачем нужны фантомные ссылки, но Вы меня заинтриговали.
Я правда не уловил момента, зачем в отдельном треде слушать очередь, если можно просто порождать тред в методе finalize() и в нем делать все то же самое (если уж очень хочется в своем треде все сделать).
Если единственная мотивация - опаска того, что будет создана жесткая ссылка во время первой сборки мусора, то это же очень легко можно контролировать, ведь они просто так из неоткуда не возьмутся раз их уже нет, да и метод get() будет null возвращать, а уж в самом методе finalize() будет сразу видно выставляется она или нет.
Вообще, конечно, информации мало, никто не понимает как это толком работает, выуживаем информацию эмперически, так что в следующих версиях на неё может и опираться нельзя будет. Так что, наверное, стоит воздержаться от их использования. Хотя в академических целях и интересно покопаться.

Mikhail Antonov комментирует...

Между прочим, маленькое уточнение для тех, кто возможно не знает. Существует флаг JVM (по крайней мере в Hotspot), позволяющий отключить вызов GC при вызове System.gc(). Вызывается -XX:-DisableExplicitGC.

BegemoT комментирует...

Небольшое замечание: в классе MyPollingThread поле-ссылку на очередь нужно сделать final. Иначе видимость значения, присвоенного в конструкторе, в методе run -- не гарантируется (ну, скажем так -- я не вижу, чем она гарантируется)

robotrader комментирует...

А вызвать ReferenceQueue.remove() один раз вместо того, чтобы поллить каждые 50мс?

Pavel Samolisov комментирует...

А где гарантия, что ссылок на объект к этому времени уже не будет?

Ivan Pavlukhin комментирует...

>>robotrader комментирует...
>>А вызвать ReferenceQueue.remove() один раз вместо того, чтобы поллить каждые 50мс?

>>Pavel Samolisov комментирует...
>>А где гарантия, что ссылок на объект к этому времени уже не будет?

Судя из документации, ReferenceQueue.remove() блокируется до тех пор, пока ссылка не появится в очереди. В очереди она появиться может только тогда, когда на целевой объект исчезнут все более сильные ссылки. К тому же, если ReferenceQueue.poll() гарантирует нам, "что ссылок на объект к этому времени уже не будет", то ReferenceQueue.remove() дает нам не меньше гарантий.

Еще хочется заметить, что в доках пишут, что использовать отдельный поток для опроса ReferenceQueue не очень хорошая идея:
While some programs will choose to dedicate a thread to removing reference objects from one or more queues and processing them, this is by no means necessary. A tactic that often works well is to examine a reference queue in the course of performing some other fairly-frequent action. For example, a hashtable that uses weak references to implement weak keys could poll its reference queue each time the table is accessed. This is how the WeakHashMap class works. Because the ReferenceQueue.poll method simply checks an internal data structure, this check will add little overhead to the hashtable access methods.

Pavel Samolisov комментирует...

@Ivan Pavlukhin cпасибо за ценное замечание.

Ivan Pavlukhin комментирует...

Сильно запоздалый комментарий. Одно сообщение по данной статье долгое время вызывало у меня сомнения. Но после недавнего копания в JMM, похоже, был выработан ответ.
>> BegemoT комментирует...
>> Небольшое замечание: в классе MyPollingThread поле-ссылку на очередь нужно сделать final. Иначе видимость значения, присвоенного в конструкторе, в методе run -- не гарантируется (ну, скажем так -- я не вижу, чем она гарантируется)
Итак, видимость значения, присвоенного в конструкторе MyPollingThread полю referenceQueue. Гарантируется следующим положением JMM:
A call to start() on a thread happens-before any actions in the started thread.

Это также и значит, что все, что находится в коде до вызова thread.start(), а, в частности, Thread thread = new MyPollingThread(queue); также happens-before любой операции в теле метода MyPollingThread.run(). Это нам и гарантирует видимость.

Pavel Samolisov комментирует...

О! Большое спасибо за комментарий.

Насколько я помню единственная сложность с данным правилом возможна если мы стартуем поток в конструкторе некоторого класса и в методе run() потока обращаемся к объекту данного класса, из которого и запускали поток. Тогда, если в конструкторе после запуска потока есть еще действия, то уже не факт, что в запущенном потоке мы увидим их результат.

Pavel Samolisov комментирует...

Запоздало прокомментирую

By @Tema
"Я правда не уловил момента, зачем в отдельном треде слушать очередь, если можно просто порождать тред в методе finalize() и в нем делать все то же самое (если уж очень хочется в своем треде все сделать)."

Сейчас читаю документацию по IBM JVM, затронута как раз тема finalizer'ов. Идея в том, что если у объекта переопределен метод finalize(), то данные объект не будет собран до тех пор, пока данный метод не будет выполнен. Но авторы JVM не идиоты и понимают, что данный метод может быть большим и процессоро/временеемким и поэтому если памяти хватает, то лучше удалить какие-то другие объекты из кучи, нежели объекты с переопределенным методом finalize(): The GC does not know what is in a finalizer, or how many finalizers exist. Therefore, the GC tries to satisfy an allocation without processing finalizers. If a garbage collection cycle cannot produce enough normal garbage, it might decide to process finalized objects. Therefore, it is not possible to predict when a finalizer is run. и я бы добавил, что вообще будет ли "is run at all". Вообще вероятнее всего будет, но гарантировать этого никто не может.

Так же в IBM JVM финализация выполняется в другом потоке нежели сборка мусора асинхронно по отношению к последней.

Алексей Вахтин комментирует...

Честно говоря, копаю интернет, и до сих пор не совсем осознаю зачем нужны мягкие, слабые и тем более фантомные ссылки, и чем фантомные отличаются от мягких. Если только для того, что есть объекты, которые "вдруг пригодятся" (тот же кэш), а потмо когда требуется память, они удаляются автоматически. И контроль, как я понимаю, удаления, чтобы не удалить нужный объект используется тот же finalize объекта. Поправьте, если я не прав.

Unknown комментирует...

Вот в этой статье (https://dzone.com/articles/weak-soft-and-phantom-references-in-java-and-why-they-matter) говорится о том, что в очередь попадает PhantomReference перед вызовом finilize() метода.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!