夯实 Java 基础4 - 强软弱虚引用

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 基本原理与使用场景

  • 基本原理
  1. 当一个对象 soft 软可达时,在一个合适的时机会回收该 soft 对象,在gc的过程中,会做如下逻辑:如果没有设置队列,则到此结束;如果设置了队列,则将其软引用 softReference 对象赋值给 Reference.pending 对象
  2. 之后 Reference.ReferenceHandler 线程(该线程在 Reference.static 块中创建启动)扫描 Reference.pending 对象
  3. 如果 Reference.pending == null,则线程阻塞等待被唤醒(gc完成之后jvm会做唤醒);如果 Reference.pending != null,则判断 Reference.pending instanceof Cleaner,如果是,则直接执行其 Cleaner.clean();否则
  4. 将其软引用 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;
}

实际上回收的时机就是:

  1. soft 对象软可达(没有强引用)
  2. 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 基本原理

  • 基本原理
  1. 当一个对象 weak 弱可达时,在下一次 gc时会回收该 weak 对象,在 gc 的过程中,会做如下逻辑:如果没有设置队列,则到此结束;如果设置了队列,则将其弱引用 weakReference 对象赋值给 Reference.pending 对象
  2. 之后 Reference.ReferenceHandler 线程(该线程在 Reference.static 块中创建启动)扫描 Reference.pending 对象
  3. 如果 Reference.pending == null,则线程阻塞等待被唤醒(gc 完成之后,jvm 做唤醒操作);如果 Reference.pending != null,则判断 Reference.pending instanceof Cleaner,如果是,则直接执行其 Cleaner.clean();否则
  4. 将其软引用 weakReference 对象加入 ReferenceQueue,之后我们可以对 weakReference 对象进行一些操作。

2.3 对象回收时机

下一次 gc 时

2.4 经典使用场景

  • JDK WeakHashMap:大致原理如下
===================== MyWeakHashMap ==========================
public class MyWeakHashMap {
    /** 引用队列 */
    private final ReferenceQueue refQ = new ReferenceQueue<>();
    private Entry[] table;

    public MyWeakHashMap(int n) {
        table = (Entry[]) new Entry[n];
    }

    class Entry extends WeakReference {
        private K key;
        private V value;
        Entry next;

        public Entry(K key, V value) {
            /**
             * key是强引用,Entry是一个WeakReference,包裹了key;
             * 当key弱可达且被gc回收时,Entry赋值pending,并且Entry进行入refQ
             */
            super(key, refQ);
            this.value = value;
        }
    }

    public V get(K key) {
        /** 在 get()时进行清除操作 */
        expungeStaleEntries();
        return table[0].value;
    }

    private void expungeStaleEntries() {
        /** 清除无效的Entry */
        for (Object x; (x = refQ.poll()) != null; ) {
            System.out.println("清除无效的Entry");
        }
    }
}

  • 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 {
    // 引用队列
    private static final ReferenceQueue dummyQueue = new ReferenceQueue();
    // 链头
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    // 堆外内存释放任务对象(Deallocator 对象)
    private final Runnable thunk;

    // 将 var0 添加到双向链表的链头
    private static synchronized Cleaner add(Cleaner var0) {
        if(first != null) {
            var0.next = first;
            first.prev = var0;
        }
        first = var0;
        return var0;
    }

    // 将当前的 Cleaner 对象 var0 移出双链表(使用 synchronized 防并发)
    private static synchronized boolean remove(Cleaner var0) {
        ...
    }

    private Cleaner(Object var1, Runnable var2) {
        // var1(即 DirectByteBuffer 对象作为 key)
        // 当 DirectByteBuffer 对象虚可达且被 gc 时,最终加入队列
        super(var1, dummyQueue);
        // 初始化堆外内存释放任务对象(Deallocator 对象)
        this.thunk = var2;
    }

    public static Cleaner create(Object var0, Runnable var1) {
        // 添加新建的 Cleaner 对象到双向链表中
        return add(new Cleaner(var0, var1));
    }

    public void clean() {
        // 将当前的 Cleaner 对象移出双链表
        if(remove(this)) {
            // 执行 Deallocator.run() 释放堆外内存
            this.thunk.run();
        }
    }
}

================ Unsafe ==================
    public native long allocateMemory(long var1);
    public native void freeMemory(long var1);

我们知道 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

你可能感兴趣的:(夯实 Java 基础4 - 强软弱虚引用)