Статьи

Знайте серию JVM — когда слабее лучше: понимание слабых, слабых и фантомных ссылок

Сколько раз мы создавали различные экземпляры объектов и присваивали их ссылочным переменным? Мы все хорошо знаем, что в Java есть автоматическая сборка мусора; так что мы просто поиграем со ссылочными переменными, и как только эти переменные будут присвоены нулю или выпадут из области видимости, JVM позаботится об этом. Не нужно беспокоиться о свободе, как в C / C ++. Это подход без головной боли, который сводит к минимуму риск возникновения утечек памяти в наших программах, и он отлично работает изо дня в день в миллиардах Java-приложений, работающих там 24 × 7. Спасибо Джону Маккарти за изобретение GC для Lisp и всем, кто реализовал эту концепцию в Java.

Но бывают случаи, когда мы немного больше контролируем процесс сбора мусора. Я не говорю о мрачном искусстве настройки сборщика мусора (о котором я расскажу в следующей статье). Это касается программных ситуаций, когда мы ожидаем, что некоторые экземпляры объектов будут иметь право на сборку мусора, чтобы высвободить некоторую нежелательную память, которая может накапливаться со временем. Ну, классическое решение явного присваивания нуля может помочь нам; учитывая, что этот конкретный объект упоминается только через эту конкретную переменную ref. Что делать, если присвоение нуля не решает проблему?

Рассмотрим сценарий, в котором вам необходимо реализовать кэш объектов. У вас есть несколько объектов, которые довольно дорого построить. Мы хотели бы хранить объекты в кэше как можно дольше (на случай, если компонент приложения должен использовать его), но мы хотим, чтобы неиспользуемые объекты из нашего кэша были освобождены, когда нам нужна память для других важные операции приложения. Если мы хотим реализовать это, используя стандартные (сильные) ссылки, это будет довольно сложно реализовать. В тот момент, когда мы добавляем объект в коллекцию, мы поддерживаем (сильную) ссылку на экземпляр, что делает его непригодным для сбора мусора. Если кэш будет продолжать расти, мы могли бы исчерпать память, что сделает точку утечки памяти для приложения. Очевидным решением будет ограничение размера кэша и удаление более старых объектов из кэша,делая эти экземпляры подходящими для GC. Хорошо, это механизм, используемый многими реализациями кеша, и он прекрасно работает. Но недостатком является то, что наш кэш ограничен по размеру, и поэтому, хотя у нас больше свободной памяти, мы не можем использовать ее. Кроме того, поскольку в кеше всегда будут какие-то ссылки, он продолжит выделять значительный блок памяти на время жизни приложения (например, если кэш-память зафиксирована на 1024 ссылках, память, используемая этими 1024 ссылками, не будет вышел). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.это механизм, используемый многими реализациями кеша, и он отлично работает. Но недостатком является то, что наш кэш ограничен по размеру, и поэтому, хотя у нас больше свободной памяти, мы не можем использовать ее. Кроме того, поскольку в кеше всегда будут какие-то ссылки, он продолжит выделять значительный блок памяти на время жизни приложения (например, если кэш-память зафиксирована на 1024 ссылках, память, используемая этими 1024 ссылками, не будет вышел). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.это механизм, используемый многими реализациями кеша, и он отлично работает. Но недостатком является то, что наш кэш ограничен по размеру, и поэтому, хотя у нас больше свободной памяти, мы не можем использовать ее. Кроме того, поскольку в кеше всегда будут какие-то ссылки, он продолжит выделять значительный блок памяти на время жизни приложения (например, если кэш-память зафиксирована на 1024 ссылках, память, используемая этими 1024 ссылками, не будет вышел). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.хотя у нас больше свободной памяти, мы не можем ее использовать. Кроме того, поскольку в кеше всегда будут какие-то ссылки, он продолжит выделять значительный блок памяти на время жизни приложения (например, если кэш-память зафиксирована на 1024 ссылках, память, используемая этими 1024 ссылками, не будет вышел). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.хотя у нас больше свободной памяти, мы не можем ее использовать. Кроме того, поскольку в кеше всегда будут какие-то ссылки, он продолжит выделять значительный блок памяти на время жизни приложения (например, если кэш-память зафиксирована на 1024 ссылках, память, используемая этими 1024 ссылками, не будет вышел). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.память, используемая этими 1024 ссылками, не будет освобождена). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.память, используемая этими 1024 ссылками, не будет освобождена). Да, есть способы решения каждой из этих проблем (например, динамически увеличивать / уменьшать кэш), но это требует некоторого честного кодирования, чтобы сделать это. Это был практический вопрос, с которым я сталкивался в одном из моих проектов в прошлом.

Если бы был лучший (и более простой) способ сделать это …

Это решение долгое время было частью JDK, со времен Java 2. Познакомьтесь с пакетом java.lang.ref, где для решения таких проблем могут использоваться ссылки Soft, Weak и Phantom. Ссылки, которые мы создаем с помощью оператора присваивания, называются сильными ссылками, поскольку приложение строго ссылается на экземпляр, что делает его непригодным для сбора мусора.

Object obj = new Object(); // Strong Ref

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

Слабая ссылка может быть создана для экземпляра следующим образом (все ссылочные типы доступны в пакете java.lang.ref).

WeakReference<Object> weakRef = new WeakReference<Object> (obj);

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

Object strongRef = weakRef.get();

Если ссылка уже была собрана сборщиком мусора, вызов метода get вернет null.

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

public class TestRef {

    public static void main(String[] args) {

        // Initial Strong Ref
        Object obj = new Object();  

        System.out.println("Instance : " + obj);

        // Create a Weak Ref on obj
        WeakReference<Object> weakRef
                  = new WeakReference<Object>(obj);

        // Make obj eligible for GC !
        obj = null;     

        // Get a strong reference again. Now its not eligible for GC
        Object strongRef = weakRef.get();  

        System.out.println("Instance : " + strongRef);

        // Make the instance eligible for GC again
        strongRef = null;

        // Keep your fingers crossed
        System.gc();    

        // should be null if GC collected
        System.out.println("Instance : " + weakRef.get());
    }
}

И результат программы будет:

Instance : java.lang.Object@a90653
Instance : java.lang.Object@a90653
Instance : null

Теперь, когда мы рассмотрели, почему нам нужны слабые ссылки, и на практическом примере использования слабых ссылок, давайте рассмотрим некоторую теорию за пятью степенями доступности. Нижеследующее основано на JDK API Docs .

  1. Сильно достижимый — если у нас есть сильная ссылка на конкретный экземпляр, то он считается сильно достижимым. Следовательно, он не подходит для сбора мусора.
  2. Мягко достижимый — если у нас нет строгой ссылки на экземпляр, но мы можем получить доступ к объекту через SoftReference (подробнее об этом позже), то экземпляр называется мягко достижимым.
  3. Weakly Reachable — если у нас нет ни сильной, ни мягкой ссылки, но к объекту можно получить доступ через WeakReference, то говорят, что экземпляр слабо доступен.
  4. Phantomly Reachable — если у нас нет сильных, мягких или слабых ссылок на конкретный экземпляр (который не был завершен), но, если у нас есть PhantomReference (объяснено через некоторое время) на экземпляр, то говорят, что экземпляр фантомно достижим.
  5. Недоступен — если у нас нет какой-либо из вышеуказанных ссылок на экземпляр, он недоступен из программы.

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

Сильные ссылки> Мягкие ссылки> Слабые ссылки> Фантомные ссылки

Каждый из этих ссылочных механизмов служит определенной цели. Далее мы рассмотрим каждую из этих ссылок и некоторые связанные конструкции в API.

1. Мягкие ссылки

Согласно Спецификации Java API, реализациям JVM рекомендуется не очищать мягкую ссылку, если у JVM достаточно памяти. То есть, если доступно свободное пространство кучи, есть вероятность, что мягкая ссылка не будет освобождена во время цикла сборки мусора (поэтому она сохраняется из GC). Однако, прежде чем выдать OutOfMemoryError, JVM попытается восстановить память, выпуская экземпляры, которые легко доступны. Это делает Soft References идеальным для реализации чувствительных к памяти кешей (как в нашем примере проблемы).

Рассмотрим следующий пример.

public class TestSoftRef {
    public static void main(String[] args) {

        // Initial Strong Ref
        Object obj = new Object();
        System.out.println("Instance : " + obj);

       // Make a Soft Reference on obj
        SoftReference<Object> softReference =
                    new SoftReference<Object>(obj); 

        // Make obj eligible for GC !
        obj = null;     

        System.gc();    // Run GC

        // should be null if GC collected
        System.out.println("Instance : " + softReference.get());
    }
}

И выходной будет…

Instance : java.lang.Object@de6ced
Instance : java.lang.Object@de6ced

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

2. Слабые ссылки

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

3. Призрачные ссылки

Фантомные ссылки — самая слабая форма ссылок. Экземпляры, на которые ссылаются через фантомную ссылку, не могут быть доступны напрямую с помощью метода get () (он всегда возвращает ноль), как в случае мягких / слабых ссылок.

Вместо этого нам нужно полагаться на справочные очереди, чтобы использовать фантомные справки. Мы рассмотрим справочные очереди в ближайшее время. Один из вариантов использования ссылок Phantom — отслеживать активные ссылки в приложении и знать, когда эти экземпляры будут собираться мусором. Если мы используем надежные ссылки, то экземпляр не будет иметь права на GC из-за надежных ссылок, которые мы поддерживаем. Вместо этого мы могли бы полагаться на фантомную ссылку с поддержкой ссылочной очереди для обработки ситуации. Пример фантомных ссылок приведен в разделе «Очереди ссылок» ниже.

4. Справочные очереди

ReferenceQueue — это механизм, предоставляемый JVM для уведомления, когда ссылка на экземпляр собирается собирать мусор. Очереди ссылок могут использоваться со всеми ссылочными типами, передавая их конструктору. При создании PhantomReference необходимо указать справочную очередь.

Использование справочной очереди заключается в следующем.

public class TestPhantomRefQueue {

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

      Object obj = new Object();
      final ReferenceQueue queue = new ReferenceQueue();

      PhantomReference pRef =
		new PhantomReference(obj,queue);

      obj = null;

      new Thread(new Runnable() {
         public void run() {
           try {
             System.out.println("Awaiting for GC");

  	     // This will block till it is GCd
             PhantomReference pRef =
		(PhantomReference) queue.remove(); 

             System.out.println("Referenced GC'd");

            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }).start();

        // Wait for 2nd thread to start
        Thread.sleep(2000);

        System.out.println("Invoking GC");
        System.gc();
    }
}

Выход будет

Awaiting for GC
Invoking GC
Referenced GC'd

5. WeakHashMap

java.util.WeakHashMap — это специальная версия HashMap, в которой в качестве ключа используются слабые ссылки. Поэтому, когда конкретный ключ больше не используется и является сборщиком мусора, соответствующая запись в WeakHashMap волшебным образом исчезнет с карты. И магия опирается на механизм ReferenceQueue, описанный ранее, чтобы определить, когда конкретная слабая ссылка должна собираться мусором. Это полезно, когда вы хотите построить кеш на основе слабых ссылок. В более сложных требованиях лучше написать собственную реализацию кеша.

В этой довольно длинной статье мы рассмотрели основы API ссылок, предоставляемые спецификацией Java. Содержимое, которое мы обсуждали здесь, является основами API-интерфейса для ссылок, и вам может быть полезно просмотреть документы Java для API-интерфейса ссылок.

С http://blog.yohanliyanage.com/2010/10/ktjs-3-soft-weak-phantom-references/