Java软引用、弱引用、虚引用原理

3,727 阅读6分钟

在Java中总共有4中核心的引用类型——强引用、软引用、弱引用、虚引用。一般情况下我们往往用到强引用比较多,很少会遇到场景用到其他三种引用,所以对其原理的掌握就更加是一纸空白。此次,恰遇机会就正好研究一下这四种引用的原理,以解己惑。

关于强引用,因为日常使用,大家基本都比较清楚,因此本文就不探究强引用这块。除了上述的四种引用之外,还有一种引用类型,叫做FinalReference,本文也同样不作探究。本文主要探究软引用、弱引用和虚引用的原理以及区别。

源码分析

无论是SoftReference、WeakReference,还是PhantomReference,事实上都继承了Reference类。此处先直接贴出Reference的回收过程,

image-20200723004025316

在整个Reference的回收过程中,JVM层和Java层都参与了清理工作。

Java层

由于最终的清理工作是由Java层完成的,因此我们先从Java层作为切入点。

Reference数据结构

我们不妨先来看一看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. referent为引用的对象
  2. queue用来存储被清理的引用,此处queue是通过链式来存储的,而next则表示这条链的下一个节点
  3. discovered和pending就比较有意思了,它在不同情况下有着不同的含义:
    • 在平时,discovered表示DiscoveredList
    • 在对象回收阶段时,pending和discovered共同组成PendingList,此时discovered相当于next的作用

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,就清理pending,然后指针移到下一个元素。再配上外层的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此处也不进行讨论,因为平时GC时(以CMS GC为例)也不是使用的这个策略。那么接下来就是LRUCurrentHeapPolicy和LRUMaxHeapPolicy了,那这两种策略分别在什么情况下使用的呢?

这里就不贴代码了,直接说答案吧。当我们的编译模式是server的时候使用LRUMaxHeapPolicy,编译模式是client的时候则使用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();
}

和弱引用对比,貌似唯一的区别就是虚引用 referent == NULL的时候,会执行make_referent_alive操作,但是似乎好像也没啥大的区别。说起弱引用和虚引用的真正区别,其实是在Java层代码处,虚引用的get方法永远返回的都是null,也就是说虚引用就真正的相当于没有引用(不考虑使用反射获取引用对象这种奇葩情况)。

总结

从上文的分析中,我们可以得出软引用、弱引用以及虚引用的区别:

  1. 软引用会在GC的时候可能会被清理,但是频率会比较低
  2. 弱引用在GC的时候必定会被清理
  3. 虚引用引用对象无法直接使用,主要应用场景是配合ReferenceQueue跟踪垃圾回收,在GC的时候也必然会被清理

另外, 由于SoftReference、WeakReference、PhantomReference以及FinalReference是与JVM硬相关的,因此我们随意实现自己的Reference是没有意义的。

参考

[1] Java引用类型原理剖析