最近在看一些数据结构的源码的时候发现了Reference这个类,突然就想起来关于Java引用的知识,并不了解里面真实的知识,今天就来深入源码来好好学习一番。所有的源码都在java.lang.ref包下面。注意体会里面的设计模式,多想想总有收获。

基础

Java引用体系中我们最熟悉的就是强引用类型,如 A a= new A();这是我们经常说的强引用StrongReference,jvm gc时会检测对象是否存在强引用,如果存在由根对象对其有传递的强引用,则不会对其进行回收,即使内存不足抛出OutOfMemoryError。

除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference.Java额外引入这个四种类型引用主要目的是在jvm 在gc时,按照引用类型的不同,在回收时采用不同的逻辑。可以把这些引用看作是对对象的一层包裹,jvm根据外层不同的包裹,对其包裹的对象采用不同的回收策略,或特殊逻辑处理。 这几种类型的引用主要在jvm内存缓存、资源释放、对象可达性事件处理等场景会用到。

对象可达性判断

垃圾回收时会依据两个原则来判断对象的可达性:

  • 单一路径中,以最弱的引用为准
  • 多路径中,以最强的引用为准

ReferenceQueue & Reference

Reference作为SoftReference,WeakReference,PhantomReference,FinalReference这几个引用类型的父类。主要有两个字段referent、queue,一个是指所引用的对象,一个是与之对应的ReferenceQueue。Reference类有个构造函数 Reference(T referent, ReferenceQueue<? super T> queue),可以通过该构造函数传入与Reference相伴的ReferenceQueue。

ReferenceQueue本身提供队列的功能,有入队(enqueue)和出队(poll,remove,其中remove阻塞等待提取队列元素)。ReferenceQueue对象本身保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。这种元素包含Queue的方式,确实同时ReferenceQueue提供了两个静态字段NULL,ENQUEUED. Null是内部定义的一个空类。

static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();

这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (r) {
        // 如果已经enqueue了,就不需要再做
        if (r.queue == ENQUEUED) return false;
        synchronized (lock) {
            // 设置ENQUEUE
            r.queue = ENQUEUED;
            // 没有设置head的话,r开始为链表的头元素,否则就是头结点的插入
            r.next = (head == null) ? r : head;
            head = r;
            // queue长度+1;
            queueLength++;
            if (r instanceof FinalReference) {
                // 如果是FinalReference的话,VM里面的计数+1
                sun.misc.VM.addFinalRefCount(1);
            }
            // 通知所有的lock.wait,这个是Reference里面的线程ReferenceHandler调用
            lock.notifyAll();
            return true;
        }
    }
}

我们来看看Reference的线程组,wait和notify调用的实例:

private static class ReferenceHandler extends Thread {
    // 加入某个线程组里面去,取个名字
    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        for (;;) {

            Reference r;
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    Reference rn = r.next;
                    pending = (rn == r) ? null : rn;
                    r.next = r;
                } else {
                    try {
                        // 否则就进行阻塞等待,是在ReferenceQueue中调用的notify方法
                        lock.wait();
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // Fast path for cleaners
            // 如果Reference实现了Cleaner接口,那么就调用clean方法来获取我们想要的值
            // 做一些清理操作。这个类在dt.jar中实现的。
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }

            ReferenceQueue q = r.queue;
            // 将ReferenceQueue加入到Queue中
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

Reference与ReferenceQueue之间是如何工作的呢?Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。我们可以通过下面代码块来进行把SoftReference,WeakReference,PhantomReference与ReferenceQueue联合使用来验证这个机制。为了确保SoftReference在每次gc后,其引用的referent都被回收,我们需要加入-XX:SoftRefLRUPolicyMSPerMB=0参数,这个原理下文中会在讲。

/**
 * 为了确保System.gc()后,SoftReference引用的referent被回收需要加入下面的参数
 * -XX:SoftRefLRUPolicyMSPerMB=0
 */
public class ReferenceTest {
    private static List<Reference> roots = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        ReferenceQueue rq = new ReferenceQueue();

        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                while (true) {
                    try {
                        // 这个是只读取pending里面的值
                        Reference r = rq.remove();
                        System.out.println(“reference:”+r);
                        //为null说明referent被回收
                        System.out.println( “get:”+r.get());
                        i++;
                        System.out.println( “queue remove num:”+i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        for(int i=0;i<100000;i++) {
            byte[] a = new byte[1024*1024];
            // 分别验证SoftReference,WeakReference,PhantomReference
            Reference r = new SoftReference(a, rq);
            //Reference r = new WeakReference(a, rq);
            //Reference r = new PhantomReference(a, rq);
            roots.add(r);
            System.gc();

            System.out.println(“produce”+i);
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }
}

通过jstack命令可以看到对应的Reference Handler thread

“Reference Handler” #2 daemon prio=10 os_prio=31 tid=0x00007f8fb2836800 nid=0x2e03 in Object.wait() [0x000070000082b000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        – waiting on <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        – locked <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时(或者要被回收 如下文要讲的Finalizer),提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。当然,如果我们不需要这种通知机制,我们就不用传入额外的queue,默认使用NULL queue就会入队失败。

SoftReference

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。这部分需要考虑Hotspot的源码

String str=new String("abc");                                     // 强引用  
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

SoftReference的源代码

public class SoftReference<T> extends Reference<T> {
    /**
     * 时间戳时钟,垃圾回收机制更新
     */
    static private long clock;

    /**
     * 使用getter的时候会进行更新。当soft引用被声明时,VM使用这个字段,但是这个不是要求
     */
    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;
    }

}

这里需要特别注意的是:如果错误地使用了软引用,可能会引起频繁的gc,这个是大家都不想见到的,所以使用的时候还是需要注意一下。

弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。相比SoftReference来说,WeakReference对JVM GC几乎是没有影响的。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

其实任何一个引用都可以与ReferenceQueue关联。

虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

强引用(StrongReference)

就是我们平时的new,强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

FinalReference

FinalReference 引用类型主要是为虚拟机提供的,提供 对象被gc前需要执行finalize方法的对象 的机制。

FinalReference 很简单就是extend Reference类,没有做其他逻辑,只是把访问权限改为package,因此我们是无法直接使用的。Finalizer类是我们要讲的重点,它继承了FinalReference,并且是final 类型的。Finalize实现很简单,也是利用上面我们讲的ReferenceQueue VS Reference机制。

FinalizerThread

Finalizer静态代码块里启动了一个deamon线程,我们通过jstack命令查看线程时,总会看到一个Finalizer线程,就是这个原因:

/**
    * 启动一个FinalizerThread线程
    */
static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
            tgn != null;
            tg = tgn, tgn = tg.getParent());
    Thread finalizer = new FinalizerThread(tg);
    // 执行权限稍微低一些
    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
    finalizer.setDaemon(true);
    finalizer.start();
}

FinalizerThread run方法是不断的从queue中去取Finalizer类型的reference,然后执行runFinalizer释放方。我们来看一下这个方法:

private void runFinalizer() {
    synchronized (this) {
        // 如果已经执行过Finalized方法,返回
        if (hasBeenFinalized()) return;
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            // 调用finalizee方法
            invokeFinalizeMethod(finalizee);
            /* Clear stack slot containing this variable, to decrease
                the chances of false retention with a conservative GC */
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

可以看出如果finalize方法中抛出异常会被直接吃掉

如何使用Finalizer

只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用的。下文中我们简称Finalizer 型的referent为finalizee。

Finalizer的构造函数是private的,也就是不能通过new 来生成一个Fianlizer reference。只能通过静态的register方法来生成。同时Finalizer有个静态字段unfinalized,维护了一个未执行finalize方法的reference列表,在构造函数中通过add()方法把Finalizer引用本身加入到unfinalized列表中,同时关联finalizee和queue,实现通知机制。维护静态字段unfinalized的目的是为了一直保持对未未执行finalize方法的reference的强引用,防止被gc回收掉。

那么register是被VM何时调用的呢?JVM通过VM参数 RegisterFinalizersAtInit 的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用。

何时入queue

当一个finalizee 只剩Finalizer引用,没有其他引用时,需要被回收了,GC就会把该finalizee对应的reference放到Finalizer的refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法,然后finalizee对象才能被GC回收。