深入理解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<? 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“回调/通知”业务线程 并不妥当,原谅我已经词穷了,介于这个词语可以直观的反馈这个机制,还请大家见谅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容