Мягкая ссылка Java, слабая ссылка, принцип виртуальной ссылки

Java

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

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

Анализ исходного кода

Будь то SoftReference, WeakReference или PhantomReference, фактически все они наследуют класс Reference. Здесь непосредственно размещен процесс переработки ссылки.

image-20200723004025316

В течение всего процесса повторного использования эталона в работе по очистке участвуют как уровень JVM, так и уровень Java.

Слой Java

Поскольку окончательная очистка выполняется на уровне Java, мы начинаем со слоя Java в качестве точки входа.

Справочная структура данных

Давайте сначала посмотрим на структуру данных Reference.

public abstract class Reference<T> {
    private T referent;
    volatile ReferenceQueue<? super T> queue;
    Reference next;
    transient private Reference<T> discovered;
    private static Reference<Object> pending = null;
}

Это структура данных Reference, где:

  1. референт - это объект, на который ссылаются
  2. Очередь используется для хранения очищенных ссылок, где очередь хранится в цепочке, а следующая представляет собой следующий узел в цепочке.
  3. Discovered и pending более интересны, они имеют разное значение в разных ситуациях:
    • В обычное время обнаружение означает DiscoveredList.
    • На этапе повторного использования объекта ожидающие и обнаруженные вместе образуют PendingList, а обнаруженный эквивалентен следующему.

Код повторного использования слоя Java

Далее давайте взглянем на код утилизации уровня Java, который также находится в Reference.class.

static boolean tryHandlePending(boolean waitForNotify) {
  Reference<Object> r;
  Cleaner c;
  try {
    synchronized (lock) {
      if (pending != null) {
        r = pending;
        c = r instanceof Cleaner ? (Cleaner) r : null;
        pending = r.discovered;
        r.discovered = null;
      } else {
        if (waitForNotify) {
          lock.wait();
        }
        return waitForNotify;
      }
    }
  } catch (OutOfMemoryError x) {
    Thread.yield();
    return true;
  } catch (InterruptedException x) {
    return true;
  }

  if (c != null) {
    c.clean();
    return true;
  }

  ReferenceQueue<? super Object> q = r.queue;
  if (q != ReferenceQueue.NULL) q.enqueue(r);
  return true;
}

private static class ReferenceHandler extends Thread {
  public void run() {
    while (true) {
      tryHandlePending(true);
    }
  }
}

Сначала посмотрите на метод tryHandlePending, вы можете обнаружить, что вся логика относительно проста, если pending!=null, очистить ожидание, а затем переместить указатель на следующий элемент. Вкупе с внешним while(true) реализована функция очистки всего PendingList.

Уровень JVM

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

Основной код обработки уровня JVM находится в файле referenceProcessor.cpp. Основным методом является process_discovered_references(). На примере CMS GC этот метод будет вызываться на этапе FinalMarking (перемаркировка). Основная логика этого кода составляет:

ReferenceProcessorStats ReferenceProcessor::process_discovered_references(BoolObjectClosure* is_alive, OopClosure* keep_alive, VoidClosure* complete_gc, AbstractRefProcTaskExecutor* task_executor, ReferenceProcessorPhaseTimes* phase_times) {

  double start_time = os::elapsedTime();
  disable_discovery();
  _soft_ref_timestamp_clock = java_lang_ref_SoftReference::clock();
  ReferenceProcessorStats stats(total_count(_discoveredSoftRefs),
                                total_count(_discoveredWeakRefs),
                                total_count(_discoveredFinalRefs),
                                total_count(_discoveredPhantomRefs));

  // 1. 初步处理软引用
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase1, phase_times, this);
    process_soft_ref_reconsider(is_alive, keep_alive, complete_gc,
                                task_executor, phase_times);
  }

  update_soft_ref_master_clock();

  // 2. 处理软引用、弱引用、FinalReference
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase2, phase_times, this);
    process_soft_weak_final_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
  }

  // 3. FinalReference的另一端处理逻辑
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase3, phase_times, this);
    process_final_keep_alive(keep_alive, complete_gc, task_executor, phase_times);
  }

  // 4. 处理虚引用
  {
    RefProcTotalPhaseTimesTracker tt(RefPhase4, phase_times, this);
    process_phantom_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
  }

  if (task_executor != NULL) {
    task_executor->set_single_threaded_mode();
  }

  phase_times->set_total_time_ms((os::elapsedTime() - start_time) * 1000);

  return stats;
}

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

  1. Начальная обработка мягких ссылок
  2. Обработка мягких и слабых ссылок
  3. Обработка фантомных ссылок

1. process_soft_ref_reconsider

В этом методе ядро ​​в основном вызывает следующую логику:

size_t ReferenceProcessor::process_soft_ref_reconsider_work(DiscoveredList&    refs_list,
                                                            ReferencePolicy*   policy,
                                                            BoolObjectClosure* is_alive,
                                                            OopClosure*        keep_alive,
                                                            VoidClosure*       complete_gc) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
    if (referent_is_dead &&
        !policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
      iter.remove();
      iter.make_referent_alive();
      iter.move_to_next();
    } else {
      iter.next();
    }
  }
  complete_gc->do_void();
  return iter.removed();
}

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

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

Здесь should_clear_reference фактически использует режим стратегии, что означает, что этот метод отличается в разных ситуациях.В настоящее время существуют следующие стратегии:

// AlwaysClearPolicy
class AlwaysClearPolicy : public ReferencePolicy {
 public:
  virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
    return true;
  }
};

// LRUCurrentHeapPolicy
bool LRUCurrentHeapPolicy::should_clear_reference(oop p,
                                                  jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  if(interval <= _max_interval) {
    return false;
  }
  return true;
}

// LRUMaxHeapPolicy
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                             jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  if(interval <= _max_interval) {
    return false;
  }
  return true;
}

// NeverClearPolicy
class NeverClearPolicy : public ReferencePolicy {
 public:
  virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
    return false;
  }
};

Во-первых, NeverClearPolicy на самом деле не используется в JVM, поэтому здесь мы его игнорируем. AlwaysClearPolicy здесь не обсуждается, поскольку эта политика не используется в обычном сборщике мусора (в качестве примера возьмем сборщик мусора CMS). Тогда есть LRUCurrentHeapPolicy и LRUMaxHeapPolicy, При каких обстоятельствах используются эти две политики?

Я не буду публиковать код здесь, просто скажу ответ прямо.Когда наш режим компиляции — сервер, мы используем LRUMaxHeapPolicy, а когда наш режим компиляции — клиент, мы используем LRUCurrentHeapPolicy..

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

По сути, две стратегии_max_intervalЗначения различаются следующим образом:

void LRUCurrentHeapPolicy::setup() {
  _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
}

void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  max_heap -= Universe::get_heap_used_at_last_gc();
  max_heap /= M;
  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
}

Мы не исследуем преимущества и недостатки этих двух методов. Из приведенного выше кода мы можем сделать следующие выводы:

  • Механизм восстановления мягких ссылок отличается в разных ситуациях.

  • Мягкие ссылки, вероятно, будут восстановлены, когда памяти будет недостаточно.

  • Время восстановления мягких ссылок — это вычисляемый узел времени, который рассчитывается на основе исторических данных GC, не привязанных к объему памяти в прямом смысле.

2. process_soft_weak_final_refs

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

process_soft_weak_final_refs_work(_discoveredSoftRefs[i], is_alive, keep_alive, true);
process_soft_weak_final_refs_work(_discoveredWeakRefs[i], is_alive, keep_alive, true);
process_soft_weak_final_refs_work(_discoveredFinalRefs[i], is_alive, keep_alive, false);

То есть он вызывается для мягкой ссылки, слабой ссылки и FinalReference соответственно.process_soft_weak_final_refs_work()Этот метод, давайте посмотрим на этот метод,

size_t ReferenceProcessor::process_soft_weak_final_refs_work(DiscoveredList&    refs_list,
                                                             BoolObjectClosure* is_alive,
                                                             OopClosure*        keep_alive,
                                                             bool do_enqueue_and_clear) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    if (iter.referent() == NULL) {
      iter.remove();
      iter.move_to_next();
    } else if (iter.is_referent_alive()) {
      iter.remove();
      iter.make_referent_alive();
      iter.move_to_next();
    } else {
      if (do_enqueue_and_clear) { // 软引用和弱引用的情况下都为true
        iter.clear_referent();
        iter.enqueue();
      }
      iter.next();
    }
  }
  if (do_enqueue_and_clear) {
    iter.complete_enqueue();
    refs_list.clear();
  }
  return iter.removed();
}

Эта логика также относительно проста, если говорить простыми словами:

  1. Если указанный объект пуст или если указанный объект все еще активен, удалите его из очереди.
  2. Если указанный объект не является активным объектом, добавьте его в PendingList.

То есть, когда слабая ссылка дойдет до GC, все неактивные объекты будут очищены, но мягкая ссылка не обязательно будет очищена из-за предварительного скрининга предыдущей стратегии.

3. process_phantom_refs

Основная логика вызова этого метода выглядит следующим образом:

size_t ReferenceProcessor::process_phantom_refs_work(DiscoveredList&    refs_list,
                                          BoolObjectClosure* is_alive,
                                          OopClosure*        keep_alive,
                                          VoidClosure*       complete_gc) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    oop const referent = iter.referent();
    if (referent == NULL || iter.is_referent_alive()) {
      iter.make_referent_alive();
      iter.remove();
      iter.move_to_next();
    } else {
      iter.clear_referent();
      iter.enqueue();
      iter.next();
    }
  }
  iter.complete_enqueue();
  complete_gc->do_void();
  refs_list.clear();
  return iter.removed();
}

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

Суммировать

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

  1. Мягкие ссылки могут быть очищены во время GC, но частота будет ниже.
  2. Слабые ссылки должны быть очищены во время GC
  3. Виртуальный эталонный эталонный объект нельзя использовать напрямую, основной сценарий приложения — отслеживать сборку мусора с помощью ReferenceQueue, который неизбежно будет очищен во время GC.

Кроме того, поскольку SoftReference, WeakReference, PhantomReference и FinalReference жестко связаны с JVM, нам не имеет смысла произвольно реализовывать собственный Reference.

Ссылаться на

[1] Анализ принципа ссылочного типа Java