Java 提供了四种常用的引用类型:StrongReference、SoftReference、WeakReference、PhantomReference;并且提供了一个与之息息相关的 ReferenceQueue。
参考文章
- JVM源码分析之堆外内存完全解读
- https://blog.csdn.net/androidstar_cn/article/details/54710652
- https://www.jianshu.com/p/e46158238a77
- https://www.cnblogs.com/jabnih/p/6580665.html
- 一、SoftReference
- 二、WeakReference
- 三、PhantomReference
前置
Reference 对象的状态流转图
image.png引用可达性
image.png
因为有路径5存在,所以obj4是强可达;去掉路径5,因为有路径6存在,obj4是软可达;去掉路径6,因为有路径4存在,obj4是弱可达;去掉路径4,obj4不可达。
一、SoftReference
1.1 使用姿势
=========================== 不带 ReferenceQueue =============================
private static void testSoft() throws InterruptedException {
Book soft = new Book();
SoftReference softReference = new SoftReference<>(soft);
System.out.println("soft ref : " + softReference.get()); // com.study.demo.reference.Book@2a84aee7
soft = null; // 去掉强引用,此时 soft 对象只有弱引用可达,当处于某一个合适的机会会进行回收
System.gc();
System.out.println("============== after gc ==============");
System.out.println("soft ref : " + softReference.get()); // com.study.demo.reference.Book@2a84aee7
}
=========================== 带 ReferenceQueue =============================
private static final ReferenceQueue refQ = new ReferenceQueue<>();
private static void testSoftWithQ() throws Exception {
// 开启线程做出队清理操作
new Thread(()->{
Reference reference = null;
try {
// -XX:SoftRefLRUPolicyMSPerMB=0 添加该参数,这里会执行
reference = refQ.remove();
} catch (InterruptedException e) {
}
if (reference != null) {
System.out.println("清理操作" + reference);
}
}).start();
// 创建软引用
Book soft = new Book();
SoftReference softReference = new SoftReference<>(soft, refQ);
System.out.println("soft ref : " + softReference.get()); // com.study.demo.reference.Book@2a84aee7
// 去掉强引用,此时 soft 对象只有软引用可达,当处于某一个合适的机会会进行回收
soft = null;
// gc
System.gc();
System.in.read();
}
1.2 基本原理与使用场景
基本原理
:
- 当一个对象 soft 软可达时,在一个合适的时机会回收该 soft 对象,
在gc的过程中
,会做如下逻辑:如果没有设置队列,则到此结束;如果设置了队列,则将其软引用 softReference 对象赋值给 Reference.pending 对象- 之后 Reference.ReferenceHandler 线程(该线程在 Reference.static 块中创建启动)扫描 Reference.pending 对象
- 如果 Reference.pending == null,则线程阻塞等待被唤醒(
gc完成之后jvm会做唤醒
);如果 Reference.pending != null,则判断 Reference.pending instanceof Cleaner,如果是,则直接执行其 Cleaner.clean();否则- 将其软引用 softReference 对象加入 ReferenceQueue,之后我们可以对 softReference 对象进行一些操作。
- 使用场景
缓存:通常出队操作我们会放在一个单独的线程中进行执行
1.3 对象回收时机
详细的见:https://www.jianshu.com/p/e46158238a77,这里简单总结一下:
public class SoftReference extends Reference {
/** JVM初始化时,将clock初始化为now,之后每次gc,JVM底层都会更改该值 */
static private long clock;
/** 创建 SoftReference,初始化 timestamp=clock,以后每次执行一个 get() 操作,重新赋值 timestamp=clock*/
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue q) {
super(referent, q);
this.timestamp = clock;
}
/** 更新 timestamp = clock */
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
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;
}
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
}
实际上回收的时机
就是:
- soft 对象软可达(没有强引用)
clock - timestamp > (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB
- SoftRefLRUPolicyMSPerMB默认为1000,可以通过 -XX:SoftRefLRUPolicyMSPerMB=10 来设定,设置为0,则在 gc() 时,就会回收 soft 对象
- clock - timestamp 的值越大,表明 soft 对象越久没有被使用;如果此时的堆内存(get_heap_free_at_last_gc())已经“不多”,满足了上边的这个条件,那么 soft 对象就会被回收。
二、WeakReference
2.1 使用姿势
private static void testWeakWithQ() throws Exception {
// 开启线程做出队清理操作
new Thread(()->{
Reference reference = null;
try {
reference = refQ.remove();
} catch (InterruptedException e) {
}
if (reference != null) {
System.out.println("清理操作" + reference);
}
}).start();
// 创建弱引用
Book weak = new Book();
WeakReference weakReference = new WeakReference<>(weak, refQ);
System.out.println("weak ref : " + weakReference.get());
// 去掉强引用,此时 weak 对象只有弱引用可达,当下一次gc时会进行回收
weak = null;
System.gc();
System.out.println("============== after gc ==============");
System.out.println("weak ref : " + weakReference.get()); // null:gc时,弱引用对象被回收
System.in.read();
}
2.2 基本原理
基本原理
:
- 当一个对象 weak 弱可达时,在
下一次 gc时
会回收该 weak 对象,在 gc 的过程中
,会做如下逻辑:如果没有设置队列,则到此结束;如果设置了队列,则将其弱引用 weakReference 对象赋值给 Reference.pending 对象- 之后 Reference.ReferenceHandler 线程(该线程在 Reference.static 块中创建启动)扫描 Reference.pending 对象
- 如果 Reference.pending == null,则线程阻塞等待被唤醒(
gc 完成之后,jvm 做唤醒操作
);如果 Reference.pending != null,则判断 Reference.pending instanceof Cleaner,如果是,则直接执行其 Cleaner.clean();否则- 将其软引用 weakReference 对象加入 ReferenceQueue,之后我们可以对 weakReference 对象进行一些操作。
2.3 对象回收时机
下一次 gc 时
2.4 经典使用场景
- JDK WeakHashMap:大致原理如下
===================== MyWeakHashMap ==========================
public class MyWeakHashMap {
/** 引用队列 */
private final ReferenceQueue
- JDK ThreadLocal:netty源码分析2 - ThreadLocal源码解析
原理与 WeakHashMap 一致,当作为 key 的 ThreadLocal 实例无效时,整个 Entry 需要清除。
image.png
- Netty FastThreadLocal:netty源码分析3 - FastThreadLocal框架的设计
原理稍微复杂一些:当前线程作为 key,value 是当前线程的清理任务,当当前线程弱可达被回收时,执行当前线程的清理任务。
三、PhantomReference
3.1 使用姿势
private static void testPhantomWithQueue() throws InterruptedException {
// 1. 开启线程做出队清理操作
new Thread(() -> {
Reference reference = null;
try {
reference = refQ.remove();
} catch (InterruptedException e) {
}
if (reference != null) {
System.out.println("清理操作" + reference);
}
}).start();
// 2. 创建虚引用
Book phantom = new Book();
PhantomReference phantomReference = new PhantomReference<>(phantom, refQ);
// 3. 去掉虚引用,此时phantom对象只有虚引用可达,当下一次gc时会进行回收
phantom = null;
System.gc();
System.out.println("============== after gc ==============");
System.out.println("phantom ref : " + phantomReference); // com.study.demo.reference.Book@2a84aee7
}
- 与软弱不一样,虚引用必须与 ReferenceQueue 一起使用
- 与软弱不一样,虚引用无法通过 get() 获取到 referent(phantom 对象)
3.2 基本原理
与弱引用完全一致。
3.3 对象回收时机
下一次 gc
3.4 经典使用场景
JDK:Cleaner,堆外内存回收
================ ByteBuffer ==================
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
================ DirectByteBuffer ==================
protected static final Unsafe unsafe = Bits.unsafe();
private final Cleaner cleaner;
long address; // 堆外内存的内存地址
DirectByteBuffer(int cap) {
...
// 做堆外内存的阈值check -XX:MaxDirectMemorySize 可指定堆外内存大小
Bits.reserveMemory(size, cap);
// 分配内存(底层通过 mollac 进行分配)
base = unsafe.allocateMemory(size);
// 初始化内存
unsafe.setMemory(base, size, (byte) 0);
address = base; // 简化设置堆外内存地址逻辑
// 创建 Cleaner(释放堆外内存)
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
================ Deallocator ==================
private static class Deallocator implements Runnable {
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address; // 存储堆外内存地址,用于后续回收
...
public void run() {
// 释放堆外内存
unsafe.freeMemory(address);
}
}
================ Cleaner ==================
// 虚引用
public class Cleaner extends PhantomReference
我们知道 DirectByteBuffer 的内存分配如下
image.png
即在堆内存中只存在一个很小的 DirectByteBuffer 对象,但是其后隐藏占用了一块堆外内存;当 DirectByteBuffer 对象被 gc 时,Cleaner 会回收其对应的堆外内存,但是假设 DirectByteBuffer 对象一直撑到了老年代,并且迟迟没有 fullgc,则堆外的一大块儿内存将得不到释放,最终将会发生 oom;所以推荐我们主动去回收堆外内存,堆外内存怎样主动回收呢?见下边这篇文章:
http://calvin1978.blogcn.com/articles/directbytebuffer.html
"从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。clean()执行时实际调用的是被绑定的Deallocator类,这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。"
image.png