Содержание

пятница, 17 августа 2012 г.

Как использовать фантомные ссылки

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

Основная задача фантомных ссылок как и метода finalize — дать возможность выполнить некоторый код, когда объект станет готов к сборке мусора. Большое отличие фантомных ссылок от других в том, что по ним нет возможности достучаться до объекта. Т.е понятие достижимости объекта по фантомной ссылке нельзя воспринимать в прямом смысле этого слова, так как объект недостижим по ней. Отсюда и главное преимущество по ставнению с методом – не возникает проблем реордеринга и нет возможности случайно воскресить объект, о которых можно подробнее почитать тут
Тем кто знаком с java.lang.ref API может показаться весьма не удобным работа с фантомными ссылками. Пример его использования для фантомных ссылок можно найти тут. Видимо, разработчикам HotSpot тоже так показалось и они добавили специальный класс sun.misc.Cleaner. Хотя он и находиться в проприетарном пакете, доступ к нему в отличие от sun.misc.Unsafe любезно открыт всем желающим. Чтобы повесить какое-то действие подчистку объекта, зарегистрировать некий Runnable следующим способом:
Object ref = new …;
Cleaner.create(ref, new Runnable() {
    @Override
    public void run() {
    // put your code here
    }
});
фантомные ссылки элегантно рассчитаны на то, что вам не удастся воскресить объект, ведь, передавая ваш Runnable в Cleaner вам придется всю необходимую информацию явно скопировать из объекта. Причем даже если программист ошибется и засунет туда ссылку на  объект, то он никогда не станет фантомно достижимым, так как ссылка на него всегда будет оставаться доступна через статический контекст класса Cleaner.

Так же с фантомными ссылками не возникает проблемы реордеринга, ведь при финализации любого объекта у вас нет возможности получить доступ к другому уже финализированноу объекту.

А имплементация Runnable как бы намекает программисту, что код который там присутствует будет выполняться в другом потоке, нежеле код других методов класса, что иногда забывается в случае метода finalize.

Не лишним будет так же упомянуть, что код вашего Runnable будет выполнять reference-handler потоком, что конечно бонус по сравнения с finalizer потоком, так как не требует JNI вызовов внутри VM, однако какую-то тяжелую обработку все же туда пихать не стоит, так как это может замедлить очистку других объектов.

Lifecycle у Cleaner будет такой же, как если вы бы писали обработку фантомной ссылки сами: при первой сборки мусора, когда объект стал достижи только фантомно сработает ваш код Runnable. И только при следующей сборке мусора объект будет реально удален. Если же вы переопределили метод finalize, то чтобы ваш объект реально собрался, потребуется как минимум три сборка мусора: во время первой выполниться код метода finalize, в последующей — ваш код Runnable и только в третьей сборке ваш объект освободит мест ов Heap.

Еще наверное стоит обратить внимание, что не стоит через Cleaner выполнять какой-то сложный и долгий код, так как это неминуемо вызовет очередь на обработку и замедлит финализирующую работу с другими ссылками. Так же нужно понимать, что порядок в котором вы создаете свои события через Cleaner не гарантирует того, что выполняться они будут в том же порядке.

Если же вам претит использовать проприетарное API HotSpot, то можете взглянуть в сторону FinalizableReference от apache, который тоже представляет некую обертку над фантомной ссылкой, облегчая работу с ними, причем финализирующий код в этом случае запускается сторонним потоком, а не потоком reference-handler или finalizer.

Ну и в заключении наконец скажу как вы неявно используете фантомные ссылки в повседневной жизни. Дело в том, что DirectByteBuffer, который широко используется в NIO или умельцами по запихиванию данных за пределы Heap, как раз и использует вышеописанный Cleaner для освобождения нативной памяти. Так же фантомными ссылками неявно пользуются разработчики, оперирующие через java с файлами замапленными в память с помощью MappedByteBuffer, который по сути и является DirectByteBuffer в HotSpot.

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

  1. Спасибо за статью. Один вопрос меня беспокоит: для любого объекта у которого переопределен метод finalize() освобождение памяти произойдет за три прохода GC, верно?
    В статье "Использование PhantomReferences в Java" написано
    "Если у объекта переопределен метод finalize(), то при первом проходе сборщика мусора данный объект помечается как требующий удаления. Затем у него выполняется метод finalize(). Сам объект при этом удаляется только при последующих проходах сборщика мусора."

    ОтветитьУдалить
    Ответы
    1. Нет, не верно. За три если он еще и через фантомную ссылку доступен, а так за два: в первый выполниться finalize, а во второй соберется.

      Удалить
  2. Не могу понять суть следуещего:

    "фантомные ссылки элегантно рассчитаны на то, что вам не удастся воскресить объект, и передавая ваш Runnable в Cleaner вам придется всю необходимую информацию скопировать при передаче. Причем даже если программист ошибется, и засунет туда ссылку на вам объект и ссылка на него никогда не станет фантомно достижимой, так как ссылка на него всегда будет оставаться доступна через статический контекст класса Cleaner."

    Кроме того, здесь несколько граматических ошибок.

    ОтветитьУдалить
    Ответы
    1. Не понятно первое предложение, второе или их связка? Напишите конкретнее, что не понятно или какие-то свои мысли по этому поводу, а то мне так тяжело объяснить, что вас смущает.

      Ну по русскому у меня всегда твердая тройка была, так что я и не пытаюсь уже писать грамотно :)

      Удалить
    2. Ну я так понимаю, ты хотел просто сказать, что Cleaner это более наглядный способ описать "при сборке этого объекта я еще хочу сделать то-то". На самом деле, это, пожалуй, основное достоинство Cleaner. Потому что насчет "вам придется всю необходимую информацию скопировать при передаче" -- вот это как раз совершенно не очевидно. Я-то понимаю, но само API на это никак не намекает, как раз наоборот -- прямо так и хочется внутри Runnable зацепить исходную ссылку. Когда прямо через FR работаешь -- там это как-то более очевидно.

      Написано правда очень запутанно. Как будто с русского на английский и обратно автопереводчиком перевели.

      Удалить
    3. Брр, действительна какая-то мешанина, как будто смержился с конфликтами и закомитил как есть :)
      Переписал ту бредятину, как надеюсь, более по-человечески. Спасибо!

      Удалить