深入理解Reference引用

本文结合,ThreadLocal内存泄漏 和 DirectByteBuffer释放 讲解 Java 中的 Reference

四种引用类型

  • 强引用(Strong Reference):被强引用的对象,GC不能够收集。常见得强引用对象得方式有: 赋值 Object obj = new Object() ,集合引用list.add(new Object())等等。
  • 软引用(Soft Reference):被软引用的对象,GC会在即将发生内存溢出时,只要没有对它强引用,就把它纳入GC收集对象内,进行回收,软引用通过 SoftReference 来实现,它有两个构造 (T reference)(T reference,ReferenceQueue queue),和一个 get() 方法,用于获取引用的对象。
  • 弱引用(Weak Reference):被弱引用得对象,下一次GC时,只要没有对它强引用就会纳入GC收集对象内,进行回收。与 SoftReference 相同,有两个构造和一个获取引用对象得方法。
  • 虚引用(Phantom Reference):被虚引用得对象随时可以被GC,并且它不能通过get() 获取到引用对象,这个方法固定返回为 null ,存在得意义在于,可以在对象被收集时,‘得到通知’ 进而做一些其他工作,例如,DirectByteBuffer 就是利用 PhantomReference 做直接内存得释放工作得。

Reference

强引用(Strong Reference)底层实现无法感知,其他三种(Soft/Weak/Phantom Reference)均继承于 abstract class Reference,他们的两个 构造方法 和 一个 获取引用对象 的方法也 均来自于 Reference

// SoftReference 简单实现如下
public class SoftReference extends Reference {
    public SoftReference(T referent) {
        //在构造 Soft/Weak/Phantom Reference 时,一般都需要掉用父级得回调。
        super(referent);
        this.timestamp = clock;
    }
    
    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}
Refernece 状态
Reference内部状态.jpg
  1. Active:最初状态,被 GC 特殊处理,当引用可达性发生变化时,状态会变为 Pending 或者 Inactive ,具体是那个状态,依据于这个 Referece 在创建的时候是否,绑定了一个 ReferenceQueue
  2. Pending:在这个状态时,Reference 内部变量 Reference pending 会被赋值为当前的引用(这个赋值操作是 JVM 负责的) ,由内部启动得线程掉用它得 enqueu 方法进入另一个状态。
  3. Enqueue:将 Reference pending 放入到 ReferenceQueue 内并唤醒,所有在这个队列上等待得线程,
  4. Inactive:终态,到此为止,这个 Reference 再也不能更改其他状态了。
  5. GC“回调/通知”业务线程

    Reference 存在一个静态代码块会启动一个线程,私有静态成员 pending 由 垃圾收集器设置,并唤醒这个线程处理相关逻辑。摘要原代码:

    public abstract class Reference {
        //由 collector 设置,并唤醒下面启动的线程
        private static Reference pending = null;
        
        static {
            ...
            //处理逻辑,如果 pending == null 则 wait
            //否则为clearner对象,则直接调用clear() clear方法一般会开启线程,不应该阻塞这个loop
            //否则存在ReferenceQueue,则放入 queue 中并唤醒等待的用户线程
            Thread handler = new ReferenceHandler(tg, "Reference Handler");
            //最高优先级
            handler.setPriority(Thread.MAX_PRIORITY);
            //守护线程
            handler.setDaemon(true);
            handler.start();
            ...
        }
    }
     
     
    Reference线程运行流转.jpg

    ThreadLocal & WeakReference 的使用

    ThreadLocal 本质上是一个门面类。通过它设置value,本质上是,在 Thread 的成员变量 ThreadLocal.ThreadLocalMap threadLocals = null; 中放入 Entry ,其中 k 为这个 ThreadLocal 对象,value 为需要存的数值。这样的话就会有内存泄漏的风险,代码描述如下:

    //产生一个threadLocal对象
    ThreadLocal threadLocal = new ThreadLocal();
    ...
    //产生一个Thread t
    new Thread(()->{
        //某些场景下设置 ThreadLocal 变量
        //本质是在 threadLocalMap 中放入一个 Entry
        threadLocal.set(new Object());
    
        while (true) {
            //之后去使用这个内容
            threadLocal.get();
        }
    }).start();
    ...
    //在之后的某块代码,将这个ThreadLocal给设置成null了
    //之后,线程内通过这个 ThreadLocal 其实已经无法访问到期望的 value 了
    //但实际上,Entry 仍然被 threadLocalMap 强引用,占用着内存
    threadLocal = null;
     
     

    从开发角度来说,将 threadLocal = null; => threadLocal.remove() 就可以解决这个问题。从Java语言层面其实ThreadLocal机制也存在其他操作来减少内存溢出的风险。

    看一下ThreadLocalMap的实现

    static class ThreadLocalMap {
        //继承了 WeakRefernce ,Entry的key其实时一个弱饮用
        //也就是说,当ThreadLocal没有任何强引用的时候,通过 Reference#get()方法获取key就会是 null
        static class Entry extends WeakReference> {
            Entry(ThreadLocal k, Object v) {
                //k == ThreadLocal
                //Entry 的 Key 时 WeakReference,当没有强引用时,会get到null
                super(k);
                value = v;
            }
        }
        
        //这个方法内,会移除掉 key == null 的 Entry
        //这个方法会在,put 的时候,如果 key 为 null 时调用
        private void replaceStaleEntry(ThreadLocal key, Object value,
                                           int staleSlot) {
        }
    }
    

    简单来说就是,ThreadLocalMap 中 Entry K是一个 ThreadLocal 的弱引用,当 ThreadLocal 没有任何强引用时,Entry的再获取 K的时候,会得到一个 null,再下一次 put 的时候,就会从 ThreadLocalMap 中溢出掉所有 key 为 null 的 Entry。

    DirectByteBuffer & PhantomReference 的使用

    //设置堆最大最小10m,直接内存最大使用10m
    //-Xmx10m -Xms10m -XX:MaxDirectMemorySize=10m
    public static void main(String[] args) throws IOException {
        //分配10m directbuffer
        ByteBuffer buff1 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
        //GC之后,在分配10m,这里并不会内存溢出
        buff1 = null;
        //这个显示调用去掉,其实在直接内存不足的时候,也会自动出发 FullGC 
        System.gc();
        ByteBuffer buff2 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
    }
    

    看上面的代码举例,很奇怪的一点是,GC 一般来说只会对 Java堆 以及 MateSpace(1.8 方法区实现)做回收,那为什么直接内存在运行了 System.gc() 之后也仿佛被回收了呢?

    DirectByteBuffer 时怎么被释放的呢?

    答案在于 DirectByteBuffer 的创建过程,代码如下:

    DirectByteBuffer(int cap) {   // package-private    
        ...
        //关键在于这个 Cleaner,对于 DirectByteBuffer 的虚引用,并且接受一个 Runnable,这里是 Deallocator。
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    
    }
    

    这个Cleaner其实本质上是一个对 DirectByteBuffer 对象的虚引用,并且还接受了一个 Deallocator 对象(本质上时一个Runnable,核心代码是通过unsafe释放内存)。上面讲过,再 GC 时,收集器一旦发现一个引用可达发生了变化,就会走 GC“回调/通知”业务线程 这一套逻辑(上面讲过了)。从而调用了 Cleaner#clean ,在这个例子中,这个方法,其实就是运行 Dealocator ,最终通过 unsafe.freeMemory(address) 释放内存。

    总的来说就是,通过Reference(具体来说是PhantomPeference)的通知/回调机制,在回收引用对象时,运行一段用户代码,调用unsafe.freeMemory(address)释放了直接内存。

    除了 ThreadLocal DirectByteBuffer 外,其他利用 Reference 在垃圾回收时,触发一些用户操作的类,还有很多。如:WeakHashMap 利用 WeakReference 防止内存溢出。

    上述中,我这里将 Reference这个机制,叫做 GC“回调/通知”业务线程 并不妥当,原谅我已经词穷了,介于这个词语可以直观的反馈这个机制,还请大家见谅。

    你可能感兴趣的:(深入理解Reference引用)