美文网首页
深入理解Reference引用

深入理解Reference引用

作者: ________方块丶 | 来源:发表于2021-07-04 18:40 被阅读0次

本文结合,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<? super T> queue),和一个 get() 方法,用于获取引用的对象。
  • 弱引用(Weak Reference):被弱引用得对象,下一次GC时,只要没有对它强引用就会纳入GC收集对象内,进行回收。与 SoftReference 相同,有两个构造和一个获取引用对象得方法。
  • 虚引用(Phantom Reference):被虚引用得对象随时可以被GC,并且它不能通过get() 获取到引用对象,这个方法固定返回为 null ,存在得意义在于,可以在对象被收集时,‘得到通知’ 进而做一些其他工作,例如,DirectByteBuffer 就是利用 PhantomReference 做直接内存得释放工作得。

Reference

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

// SoftReference 简单实现如下
public class SoftReference<T> extends Reference<T> {
    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<Object> pending 会被赋值为当前的引用(这个赋值操作是 JVM 负责的) ,由内部启动得线程掉用它得 enqueu 方法进入另一个状态。
  3. Enqueue:将 Reference<Object> pending 放入到 ReferenceQueue 内并唤醒,所有在这个队列上等待得线程,
  4. Inactive:终态,到此为止,这个 Reference 再也不能更改其他状态了。
GC“回调/通知”业务线程

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

public abstract class Reference<T> {
    //由 collector 设置,并唤醒下面启动的线程
    private static Reference<Object> 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,v> ,其中 k 为这个 ThreadLocal 对象,value 为需要存的数值。这样的话就会有内存泄漏的风险,代码描述如下:

//产生一个threadLocal对象
ThreadLocal<Object> threadLocal = new ThreadLocal();
...
//产生一个Thread t
new Thread(()->{
    //某些场景下设置 ThreadLocal 变量
    //本质是在 threadLocalMap 中放入一个 Entry<threadLocal,Object>
    threadLocal.set(new Object());

    while (true) {
        //之后去使用这个内容
        threadLocal.get();
    }
}).start();
...
//在之后的某块代码,将这个ThreadLocal给设置成null了
//之后,线程内通过这个 ThreadLocal 其实已经无法访问到期望的 value 了
//但实际上,Entry<threadLocal,Object> 仍然被 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<ThreadLocal<?>> {
        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,V> 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引用

      本文链接:https://www.haomeiwen.com/subject/ryhvultx.html