java引用Reference详细分析

众所周知,java 中引用分为四种类型,分别为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。四种引用的强度顺序是:

StrongReference > SoftReference > WeakReference > PhantomReference

其中 StrongReference 没有具体具现化的类, Object obj = new Object() 这个就属于强引用。
想弄懂这四种引用类型,就必须先了解 ReferenceReferenceQueue 这两个类。

一. Reference 类

1.1 重要属性

private T referent; 
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
transient private Reference<T> discovered;
private static Reference<Object> pending = null;

1.1.1 四种状态

想要了解这些属性的作用,先要知道对于一个 Reference 实例,它有四种状态:

  1. Active 状态:表示这个 Reference 是新创建状态,且它对应的引用对象 referent 也没有被 gc 回收。
  2. Pending 状态:表示这个 Reference 所对应的引用对象 referent 已经被 gc 回收了,等待将这个 Reference 对象放到 queue 队列中。

因此如果这个 Reference 对象创建的时候,没有设置 queue 对象,那么就不会进入这个状态。

  1. Enqueued 状态:表示这个 Reference 对象已经被放到 queue 队列中。

Pending 状态一样,如果创建 Reference 对象时,没有设置 queue 对象,那么也不会进入这个状态。

  1. Inactive 状态:表示这个 Reference 对象最终状态,无法再改变。

如果这个 Reference 对象没有设置 queue 对象,那么当这个 Reference 所对应的引用对象 referentgc 回收后,就变成这个状态了。如果设置了 queue 对象,就必须等到有人将这个 Reference 对象从 queue 队列中取出,才会进入这个状态。

所以我们可以看出,根据有没有设置 queue 对象,Reference 状态变化分为两种:

  1. 没有设置 queue 对象。当 Reference 对象创建成功后,就是 Active 状态,等到 Reference 所对应的引用对象 referentgc 回收了,那么就进入了 Inactive 状态。
  2. 设置 queue 对象。当 Reference 对象创建成功后,就是 Active 状态,等到 Reference 所对应的引用对象 referentgc 回收了,就进入了 Pending 状态,然后等待 ReferenceHandler 线程将 Reference 状态变成 Enqueued 状态。最后等待用户将这个 Reference 对象从 queue 队列中取出,那么变成了 Inactive 状态。

1.1.2 属性作用

先看一下 Reference 的构造函数

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

当我们创建 Reference 时,没有设置 queue 对象,那么默认的 queue 就是 ReferenceQueue.NULL
当我们了解了 Reference 四种状态的意义,那么就知道它的成员属性的作用了:

  1. referent : Reference 实例所对应的引用对象。
  2. queue : 用来储存 Reference 实例的队列。
  3. next : 通过这个属性形成一个队列,用来记录这个 Reference 对象的 queue 对象中所有 Enqueued 状态的对象。

active: NULL
pending: this
Enqueued: next reference in queue (or this if last)
Inactive: this

  1. discovered : 通过这个属性形成一个队列,用来记录所有处于 Pending 状态的 Reference 对象。
  2. pending : 这个是一个静态属性,所有 Reference 对象共享的。

1.2 ReferenceHandler 内部类

1.2.1 ReferenceHandler 类

private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }

这个类的实现很简单,就不多介绍了。

1.2.2 静态代码块调用

public abstract class Reference<T> {
....
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new Reference.ReferenceHandler(tg, "Reference Handler");
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }
}

Reference 的静态代码块中,开启 ReferenceHandler 线程,名字叫 Reference Handler

所以当你用 jstack pid 查看 java 的线程栈,你会看到一个名叫 Reference Handler 线程.

1.2.3 tryHandlePending 方法

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            // 防止并发冲突
            synchronized (lock) {
                // 如果静态变量有值,pending就是一个 pending状态的 Reference 对象,需要将它变成 `Enqueued` 状态
                if (pending != null) {
                    r = pending;
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // 设置下一个 `pending` 状态的  Reference 对象
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    if (waitForNotify) {
                        // 如果没有,就等待,直到 gc 进行动作
                        lock.wait();
                    }
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }

        if (c != null) {
            c.clean();
            return true;
        }
        
        ReferenceQueue<? super Object> q = r.queue;
        // 将 Reference 对象放入自己的 queue 队列中,变成 `Enqueued` 状态
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

通过上面代码,我们直到 ReferenceHandler 类就是一个线程,它的作用就是将所有 Pending 状态的 Reference 对象,放入自己的 queue 队列中,变成 Enqueued 状态。

Reference 对象从 Active 状态变成 Pending 状态,是由 gc 帮我们做的,这块代码我没有找到。

为什么需要 ReferenceHandler 这个线程,来将 Reference 对象从 Pending 状态变成 Enqueued 状态,而不是有 gc 程序一起做了呢?
我想可能是不想影响 gc 程序的效率,所以另开一个线程,来异步处理这些事情。

二. ReferenceQueue 类

2.1 重要属性

    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }
    static ReferenceQueue<Object> NULL = new Null<>();
    static ReferenceQueue<Object> ENQUEUED = new Null<>();
    static private class Lock { };
    private Lock lock = new Lock();
    private volatile Reference<? extends T> head = null;
    private long queueLength = 0;

属性分析:

  1. Null 内部类:enqueue 方法返回 false,表示这个 ReferenceQueue 队列不能存放 Reference 对象。
  2. NULLENQUEUED : 用于特殊标记。
  3. head :表示这个 ReferenceQueue 队列的队列头。
  4. queueLength :表示这个 ReferenceQueue 队列中存放的 Reference 对象数量。

2.2 重要方法

2.2.1 enqueue 入队方法

    boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (lock) {
            ReferenceQueue<?> queue = r.queue;
            // 如果 Reference对象的 queue 是 NULL 或者 ENQUEUED,
            // 表示这个 Reference对象不能存放到queue队列中
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            // 如果 queue 不是当前 ReferenceQueue 对象,直接报错
            assert queue == this;
            // 将 r.queue 变成 ENQUEUED,表示这个Reference对象状态是Enqueued
            r.queue = ENQUEUED;
            // 使用 next 来形成链条
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }

主要修改了 Reference 对象的 queuenext 属性。

2.2.2 poll 出队方法

    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }

    private Reference<? extends T> reallyPoll() {       /* Must hold lock */
        Reference<? extends T> r = head;
        if (r != null) {
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            // head 指向队列中的下一个 Reference 对象
            head = (rn == r) ? null : rn;
            // 将 queue 设置成 null
            r.queue = NULL;
            // 将 next 设置成 自己
            r.next = r;
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

ReferenceQueue 队列中获取一个 Reference 对象,这个 Reference 对象状态从 Enqueued 变成 Inactive

如果当前 ReferenceQueue 队列没有数据,那么返回 null

2.2.2 remove 等待删除方法

    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    }

    public Reference<? extends T> remove(long timeout)
        throws IllegalArgumentException, InterruptedException
    {
        if (timeout < 0) {
            throw new IllegalArgumentException("Negative timeout value");
        }
        synchronized (lock) {
            Reference<? extends T> r = reallyPoll();
            if (r != null) return r;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            for (;;) {
                lock.wait(timeout);
                r = reallyPoll();
                if (r != null) return r;
                if (timeout != 0) {
                    long end = System.nanoTime();
                    timeout -= (end - start) / 1000_000;
                    if (timeout <= 0) return null;
                    start = end;
                }
            }
        }
    }

这个方法与 poll 方法比较,它们都能移除 ReferenceQueue 中的一个 Reference 对象,并且改变这个 Reference 对象状态。
当时 poll 方法的缺陷是你不知道 Reference 对象什么时候被加入到 ReferenceQueue 队列中。 而 remove 方法可以一直等待到 ReferenceQueue 队列中有数据。

三. PhantomReference、WeakReference 和 SoftReference

3.1 相关代码

    public class PhantomReference<T> extends Reference<T> {
       // 获取都是 0
        public T get() {
            return null;
        }
        public PhantomReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }

    }
    
    public class WeakReference<T> extends Reference<T> {
        public WeakReference(T referent) {
            super(referent);
        }
        public WeakReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }

    public class SoftReference<T> extends Reference<T> {
        static private long clock;
        private long timestamp;
        public SoftReference(T referent) {
            super(referent);
            this.timestamp = clock;
        }
        public SoftReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
            this.timestamp = clock;
        }
        public T get() {
            T o = super.get();
            if (o != null && this.timestamp != clock)
                this.timestamp = clock;
            return o;
        }
    }

观察一下这三个类,我们发现:

  1. WeakReferenceSoftReference 都有不设置 ReferenceQueue 对象构造方法,但是 PhantomReference 只有包含 ReferenceQueue 对象的构造方法,说明创建 PhantomReference 对象,必须设置对应的 ReferenceQueue 对象。
  2. PhantomReferenceget 方法直接返回 null,说明它获取不到所对应的引用对象。

3.2 示例代码

   public static class User {
        private String name;
        public User(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("User{");
            sb.append("name='").append(name).append('\'');
            sb.append('}');
            return sb.toString();
        }
    }
    public static void main(String[] agrs) throws InterruptedException {
        User user = new User("zhang");
        ReferenceQueue<User> queue = new ReferenceQueue<>();
        Reference<User> reference = new PhantomReference<>(user, queue);
        System.out.println("reference:  "+ reference);
        
        // 将引用对象设置成 null,可以让 gc 程序进行回收
        user = null;

        System.out.println("reference.get():  "+ reference.get());
        System.out.println("reference.isEnqueued():  "+reference.isEnqueued());
        System.out.println("queue.poll():  "+queue.poll());

        // 进行 gc
        System.gc();

        System.out.println("===============gc 之后=================");

        System.out.println("reference.get():  "+ reference.get());
        System.out.println("reference.isEnqueued():  "+reference.isEnqueued());
        System.out.println("queue.poll():  "+queue.poll());
    }

运行结果:

reference:  java.lang.ref.PhantomReference@60e53b93
reference.get():  null
reference.isEnqueued():  false
queue.poll():  null
===============gc 之后=================
reference.get():  null
reference.isEnqueued():  true
queue.poll():  java.lang.ref.PhantomReference@60e53b93

PhantomReference 换成 WeakReference,运行结果:

reference:  java.lang.ref.WeakReference@60e53b93
reference.get():  User{name='zhang'}
reference.isEnqueued():  false
queue.poll():  null
===============gc 之后=================
reference.get():  null
reference.isEnqueued():  true
queue.poll():  java.lang.ref.WeakReference@60e53b93

PhantomReference 换成 SoftReference,运行结果:

reference:  java.lang.ref.SoftReference@60e53b93
reference.get():  User{name='zhang'}
reference.isEnqueued():  false
queue.poll():  null
===============gc 之后=================
reference.get():  User{name='zhang'}
reference.isEnqueued():  false
queue.poll():  null

可以看出,对于 SoftReference 所引用的对象,即使调用 System.gc() 之后,不会将这个引用的对象回收。

四. 总结

从源码中我们可以得出,Reference 的主要作用有以下几点:

  1. 使用 Reference 包装一个引用对象 referent,如果这个引用对象 referentgc 回收了,那么 Reference 对象的 get 方法获取的引用对象 referent 就为 null 。这一步是由 gc 线程设置的。

注意,根据 Reference 类型不同, gc 线程何时回收 Reference 所包装一个引用对象 referent 也不同。PhantomReference 对象的 get 方法返回一直为空。

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

推荐阅读更多精彩内容