前言
文章
- 相关系列:《Java ~ Reference【目录】》(持续更新)
- 相关系列:《Java ~ Reference ~ SoftReference【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Reference ~ SoftReference【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Reference ~ SoftReference【问题】》(学习解答/持续更新)
一 概述
简介
SoftReference(软引用)类是Reference(引用)抽象类的四大子类之一,只被软引用所持有的对象被称为软可达(softly reachable)对象。在Java中关于软引用的定义是:如果一个对象只存在软引用,则其在空闲内存充裕的情况下不一定会被GC回收,具体情况视回收策略而定。而如果GC回收后空闲内存依旧不足,便会强制回收软可达对象以避免OOM。虚拟机在抛出OOM错误前会保证清理所有的软可达对象。
软引用类适用于缓存。软引用类适合做缓存是因为其自身可有可无的引用强度决定的,缓存被用于加快程序运行效率而非正常运行的必要条件,即使是在没有共存强引用的环境下,只要空闲内存充裕,软引用自身也能够保证热点数据持久存在,这与缓存的运用场景是高度契合的。因此使用软引用类制作缓存一方面充分利用了空闲内存加快程序的运行效率,另一方面也保证了在空闲内存不足时将仅有的内存资源用于保障程序的正常运行而非浪费在可有可无的地方。
二 使用
创建
-
public SoftReference(T referent) —— 创建指定所指对象但未注册引用队列的软引用。
-
public SoftReference(T referent, ReferenceQueue<? super T> q) —— 创建指定所指对象及注册引用队列的软引用。
方法
软引用类没有定义新方法,其所有方法皆继承/实现自引用抽象类。
-
public T get() —— 获取 —— 获取当前软引用的所指对象,当所指对象不存在时返回null。所指对象初始是必然存在的,但可以在后期被清除。
软引用类重写了get()方法,在原本直接获取所指对象的基础上新增记录了时间戳的功能,该功能被用于判断当前软引用是否可被GC回收,该知识点会在下文详述。 -
public void clear() —— 清除 —— 清除当前软引用的所指对象(即断开两者的引用关系),并不会将当前软引用加入到注册引用队列中。该方法专为开发者提供,GC线程不会调用该方法断开当前软引用与其所指对象的关联。
-
public boolean isEnqueued() —— 是否入队 —— 判断当前软引用是否已加入注册引用队列,是则返回true;否则返回false。引用加入注册引用队列时会将自身注册的引用队列替换为“入队”引用队列,这是一个在引用队列类内部创建的全局静态引用队列,被作为引用加入注册引用队列的标志位来使用。因此判断当前引用是否加入注册引用队列无需遍历注册引用队列,直接判断注册引用队列是否是“入队”引用队列即可。
-
public boolean enqueue() —— 入队 —— 将当前软引用加入注册引用队列中,成功返回true;否则返回false。该方法底层调用引用队列类的enqueue(Reference<? extends T> r) 方法实现。该方法专为开发者提供,“引用处理器”线程不会调用该方法将当前软引用加入注册引用队列。
三 实现
回收策略
回收策略是在空闲内存充裕的情况下用于回收软可达对象的规则。LRUCurrentHeapPolicy(最近最少使用当前堆策略)是最常用的回收策略(其它的我也不知道),全称为Least Recently Used Current Heap Policy,核心思想是将使用最为频繁的部分软可达对象保留,而将使用较少甚至不使用的软可达对象回收。为此其会计算软可达对象的大致闲置时间,这依赖于软引用类实现。
软引用类新组合了[clock & 时钟]和[timestamp & 时间戳]两个字段,前者由GC负责更新,用于保存最新一次GC发生的时间戳,而后者则用于保存最新一次get()方法调用时从[时钟]中拷贝的时间戳。当软引用的get()方法被调用时,如果所指对象不为null,并且[时钟]与[时间戳]并不相等,则[时间戳]会被赋值为[时钟]的值。相关源码如下:
/**
* Returns this reference object's referent. If this reference object has been cleared, either by the program or by the garbage
* collector, then this method returns <code>null</code>.
* 返回当前引用对象的所指对象。如果当前引用对象已经被程序或GC清理,那么该方法会返回null。
*/
@Override
public T get() {
T o = super.get();
// 当所指对象不为空且时间戳不等于时钟时,更新时间戳为时钟。
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
当GC判断某个软可达对象是否可被回收时,会计算其软引用的[时钟]与[时间戳]的间隔(即[时钟]减去[时间戳]的差值),并判断间隔是否大于预设的最大间隔。是则说明该软可达对象闲置时间相对过长,使用频率较低,会被GC判定为可回收;否则便会继续保留。相关预设最大间隔的规则及C++判断源码如下所示:
最大间隔 = 空闲堆内存(MB) * -XX:SoftRefLRUPolicyMSPerMB(默认值1000ms)
假设空闲堆内存为500MB,则理论上软可达对象只有闲置超过500s才会被GC判定为可回收。
bool LRUCurrentHeapPolicy::should_clear_reference(oop p, jlong timestamp_clock) {
// 使用时钟 - 时间戳得到差值,这个差值可以近似代表软可达(softly reachable)对象上次被获取时间至当前时间的间隔。
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 0, "Sanity check");
// 如果间隔 <= 最大间隔,则不进行回收。
if(interval <= _max_interval) {
return false;
}
// 如果间隔 > 最大间隔,则进行回收。
return true;
}
void LRUCurrentHeapPolicy::setup() {
// 最大间隔 = 空闲堆内存(MB) * 每MB空闲内存保留时间。
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
注意:回收策略只会在空闲内存充裕的情况下发挥作用,而当GC结束后如果依然存在空闲内存不足的情况,则即使软可达对象符合保留条件也一样会被GC清除。JVM在自动抛出OOM之前会确保所有的软可达对象都已被清除(手动抛出的不算在内)。
四 JVM参数
- -XX:SoftRefLRUPolicyMSPerMB —— 设置每MB空闲内存的单位保留时间(毫秒)。默认1000毫秒。