понеділок, 28 серпня 2017 р.

Корни, ссылки, достижимость в Java

Как сборщик мусора в Java определяет, что объект пора уничтожить? В общем случае, объект должен оказаться "недостижим" из "корневых" объектов, но всегда ли это так? Если мы говорим о жестких ссылках, то да. Однако в Java существует 4 вида ссылок:

  • жесткие;
  • мягкие (SoftReference);
  • слабые (WeakReference);
  • фантомные (PhantomReference).

Что-то много непонятного, давайте разберемся чем они отличаются, что значит "недостижим" и кто такие эти "корневые" объекты.

Для того, чтобы действительно разобраться в ссылках нужно познакомиться с термином reachability (достижимость) и очередью ссылок. То, что я буду рассказывать можно найти по ссылкам в конце поста на языке оригинала. Вообще говоря, весь пакет java.lang.ref направлен на обеспечение определенного уровня взаимодействия со сборщиком мусора. К слову, объект на который ссылается ссылка называется референтом.

Очередь ссылок - это механизм для оповещения об изменении статуса достижимости объекта. В тот момент, когда сборщик мусора определяет то, что объект более "недостижим", то он добавляет соответствующую ссылку в указанную при создании очередь (каждой ссылке можно указать в конструкторе очередь). Из очереди можно получить ссылку и в этот момент она оттуда удалится. При необходимости, делать это стоит в участках кода, которые часто выполняются, чтобы не выделять отдельный поток для очистки очереди ссылок. А что же значит "объект недостижим"?

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

  • Объект достижим в сильном смысле (strongly reachable), если он может быть достижим каким-либо потоком (thread) без обхода ссылочных объектов. Новосозданный объект достижим потоком, который его создал.
  • Объект мягко достижим (softly reachable), если он не достижим в строгом смысле, но может быть достижим путем обхода мягких ссылок, которые достижимы сами по себе.
  • Объект слабо достижим, если он не достижим ни в слабом ни в сильном смысле, но до него можно добраться через слабые ссылки. Когда слабые ссылки на слабо достижимый объект очищены (вызван метод clear()), объект получает право на финализацию.
  • Объект фантомно достижим, если на него нет ни строгих, ни мягких ни слабых ссылок, он был финализирован и только какая-то фантомная ссылка указывает на него.
  • Наконец, объект недостижим и , следовательно, может быть уничтожен, когда до него нельзя добраться никаким из перечисленных путей.

Из описанных выше уровней становится очевиден смысл каждого вида ссылки, давайте посмотрим.

Жесткие ссылки - это обычные ссылки на объекты, которые разбросаны по всему приложению. Типичный пример Object a = new Object();. Если объект достижим по такой ссылке, то сборщик мусора не сможет его уничтожить.

Мягкие ссылки (SoftReference). Объявление мягких ссылок выглядит следующим образом SoftReference<SomeClass> ref = new SoftReference<>(new SomeClass());. Получить сам объект по ссылке можно с помощью метода get(), если объект доступен через SoftReference, то сборщик мусора уничтожит его, только при высокой вероятности переполнения памяти, т.е. как один из крайних способов очистить её.

В случае если объект уничтожается, то метод get возвращает null. Пишут, что такие ссылки хороши для создания чувствительного к наличию памяти кеша (и по документации именно для этого мягкие ссылки и предназначены), но, пожалуйста, используйте готовые инструменты для кеширования, не плодите велосипеды.

Слабые ссылки (WeekReference) - для удаления объекта доступного по слабой ссылке сборщику мусора не обязательно условие нехватки памяти. Т.е. сборщик может удалить такой объект в любой момент цикла очистки. Создать слабую ссылку можно аналогичным путём: WeakReference<SomeClass> ref = new WeakReference<>(new SomeClass()); На основе ссылок такого типа работает класс WeakHashMap. Предназначение таких ссылок судя по документации это реализация "канонизирующего соответствия" (проще говоря, создание map'ы) при котором сборщику мусора не запрещается собирать ключи (или значения). Интересно, что инстанс WeakHashMap может сказать о своем размере, только вот этот размер не обязательно отражает реальную картину. Ключи могли быть уже собраны как мусор, а метод size показывает старое значение.

Мягкие и слабые ссылки автоматически очищаются сборщиком перед тем как они будут добавлены в очередь с которой они были ассоциированы, если такая была, по этому, если только вы хотите получить какое-то подтверждение того, что ссылка очищена, то вам пригодится очередь. Однако, эти ссылки очищаются попадают в очередь перед выполнением метода finalize(), а значит, если не опытный разработчик проставит жесткую ссылку на this внутри этого метода, то всё тщетно и объект будет спасён. Не делайте так. Второй раз метод finalize() вызван не будет.

Фантомные ссылки (PhantomReference) - при создании фантомных ссылок обязательно нужно указывать в конструкторе объект очереди ReferenceQueue, это связано с целью применения данного вида ссылок. Метод get у такой ссылки всегда возвращает null, спросите, а почему? Давайте разберемся. Основное применение фантомной ссылки - это создание альтернативного механизма освобождения ресурсов у объекта. Объект будет достижим, пока все фантомные ссылки на него не будут очищены, либо сами не станут недостижимы. В целях обеспечения невозможности создать жесткую ссылку на объект после его финализации, метод get всегда возвращает вам null. Когда на объект остаются только фантомные ссылки, его уровень достижимости начинает спускаться до фантомного, он финализируется (выполняется finilize()). После очистки ссылки (clear()) вручную объект станет полностью недостижим, фантомная ссылка добавится в очередь, указанную при создании, и сразу, либо спустя время, референт будет собран сборщиком мусора.

Как может помочь такой вид ссылок? Представьте, что у вас есть 2 объекта, основной и дополнительный. Вы не можете уничтожить основной раньше дополнительного в соответствии с политикой очистки ресурсов. Итого, мы оставляем две фантомные ссылки на каждый из объектов и ассоциируем их с очередью. Уровень достижимости таких объектов опускается до фантомного (см. выше), объект финализируется и единственное, что о нем напоминает в вашем коде, это фантомная ссылка. Мы вызываем clear у дополнительного объекта и начинаем опрашивать очередь ссылок, а когда метод poll очереди вернет нам ссылку на доп. объект, мы получили гарантии того, что объект финализирован и недостижим, теперь мы можем сделать clear для основного объекта. Это более гибко, чем рассчитывать на метод finalize, который выглядит как уродливое наследие.

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

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

В effective java Bloch писал о том, что этот метод замедляет работу сборки объекта в 430 раз и, вообще говоря, может быть не вызван, если вы где-то забыли ссылку на объект или программа завершит работу после того как объект можно собрать, но до вызова метода finalize.

Ну и конечно, я обещал рассказать о рутах или корневых объектах. Каждое дерево объектов должно иметь один или несколько корней до тех пор пока приложение может достигнуть этих корней всё поддерево считается достижимым. Корни считаются достижимыми всегда. Всего есть четыре типа корней в Java:

  • Локальные переменные, которые остаются живыми в соответствии со стеком потока.
  • Активные потоки в Java всегда считаются живыми объектами и следовательно они корни для сборщика мусора.
  • Статические переменные. Вообще говоря, классы тоже могут быть собраны сборщиком мусора, когда удалены все ссылки на статические переменные, это можно наблюдать, когда мы используем сервера приложений, OSGI контейнеры или в общем случае загрузчики классов.
  • JNI (Java Native Interface) ссылки, которые являются Java объектами и созданы как часть JNI вызова. Они обрабатываются отдельно, потому что JVM не знает о том, есть ли в нативном коде использование объектов доступных по ссылкам.
Достижимость объектов от корней

В простом Java приложении корневые объекты, это локальные переменные в основном потоке, сам основной поток и статические переменные основного класса.

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


Ссылки:

Оригинал документации о достижимости

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

Про корни деревьев достижимости

Немає коментарів:

Дописати коментар

HyperComments for Blogger

comments powered by HyperComments