懵!啥是Java软引用、弱引用、虚引用?

小Hub领读:

深层次分析,有谁看懂了,点个赞看看?我一脸懵逼进来,一脸懵逼出去~


作者:木枣粽子

https://juejin.im/post/6854573215767855117

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

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

源码分析

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

在整个 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 的功能。搜索公纵号:MarkerHub,关注回复[ vue ]获取前后端入门教程!

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 是没有意义的。


(完)

MarkerHub文章索引:(点击阅读原文直达)

https://github.com/MarkerHub/JavaIndex
【推荐阅读】
企业服务内部接口校验方案

为什么MySQL不推荐使用uuid或者雪花id作为主键?

Docker 实战总结(非常全面)这是我读过写得最好的【秒杀系统架构】分析与实战!
你的登录接口真的安全吗?

权限管理系统之集成Shiro实现登录、url和页面按钮的访问控制

Vue项目使用拦截器和JWT验证 完整案例

好文章!点个在看!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值