Java四种引用类型详解

前言

为了知足对不一样状况的垃圾回收需求,从Java从版本1.2开始,引入了4种引用类型(实际上是额外增长了三种)的概念。本文将详细介绍这四种引用。html

Java 4种引用类型

Java中的4中引用类型分别为强引用(String Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference)。java

概念及应用场景

> - 强引用:Java中的引用,默认都是强引用。好比new一个对象,对它的引用就是强引用。对于被强引用指向的对象,就算JVM内存不足OOM,也不会去回收它们。 > - 软引用:若一个对象只被软引用所引用,那么它将在JVM内存不足的时候被回收,即若是JVM内存足够,则软引用所指向的对象不会被垃圾回收(其实这个说法也不够准确,具体缘由后面再说)。根据这个性质,软引用很适合作内存缓存:既能提升查询效率,也不会形成内存泄漏。 > - 弱引用:若一个对象只被弱引用所引用,那么它将在下一次GC中被回收掉。如ThreadLocalWeakHashMap中都使用了弱引用,防止内存泄漏。 > - 虚引用:虚引用是四种引用中最弱的一种引用。咱们永远没法从虚引用中拿到对象,被虚引用引用的对象就跟不存在同样。虚引用通常用来跟踪垃圾回收状况,或者能够完成垃圾收集器以外的一些定制化操做。Java NIO中的堆外内存(DirectByteBuffer)由于不受GC的管理,这些内存的清理就是经过虚引用来完成的。缓存

引用队列

引用队列(Reference Queue)是一个链表,顾名思义,存放的是引用对象(Reference对象)的队列。 软引用弱引用能够和一个引用队列配合使用,当引用所指向的对象被垃圾回收以后,该引用对象自己会被添加到与之关联的引用队列中,从而方便后续一些跟踪或者额外的清理操做。 由于没法从虚引用中拿到目标对象,虚引用必须和一个引用队列配合使用。ide

案例解析

设置JVM的启动参数为 > -Xms10m -Xmx10mthis

public class ReferenceTest {
    private static int _1MB = 1024 * 1024;
    private static int _1KB = 1024;

    public static void main(String[] args) throws InterruptedException {
        // 引用队列,存放Reference对象
        ReferenceQueue queue = new ReferenceQueue();
        // 定义四种引用对象,强/弱/虚引用为1kb,软引用为1mb
        Byte[] strong = new Byte[_1KB];
        SoftReference<byte[]> soft = new SoftReference&lt;&gt;(new Byte[_1MB], queue);
        WeakReference<byte[]> weak = new WeakReference&lt;&gt;(new Byte[_1KB], queue);
        PhantomReference<byte[]> phantom = new PhantomReference&lt;&gt;(new Byte[_1KB], queue);

        Reference<string> collectedReference;
        // 初始状态
        System.out.println("Init: Strong Reference is " + strong);
        System.out.println("Init: Soft Reference is " + soft.get());
        System.out.println("Init: Weak Reference is " + weak.get());
        System.out.println("Init: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("Init: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
        System.out.println("********************");

        // 第一次手动触发GC
        System.gc();
        // 停100ms保证垃圾回收已经执行
        Thread.sleep(100);

        System.out.println("After GC: Strong Reference is " + strong);
        System.out.println("After GC: Soft Reference is " + soft.get());
        System.out.println("After GC: Weak Reference is " + weak.get());
        System.out.println("After GC: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("After GC: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
        System.out.println("********************");

        // 再分配1M的内存,以模拟OOM的状况
        Byte[] newByte = new Byte[_1MB];

        System.out.println("After OOM: Strong Reference is " + strong);
        System.out.println("After OOM: Soft Reference is " + soft.get());
        System.out.println("After OOM: Weak Reference is " + weak.get());
        System.out.println("After OOM: Phantom Reference is " + phantom.get());
        do {
            collectedReference = queue.poll();
            System.out.println("After OOM: Reference In Queue is " + collectedReference);
        }
        while (collectedReference != null);
    }
}

上述代码的输出结果为:线程

Init: Strong Reference is [Ljava.lang.Byte;@74a14482
Init: Soft Reference is [Ljava.lang.Byte;@1540e19d
Init: Weak Reference is [Ljava.lang.Byte;@677327b6
Init: Phantom Reference is null
Init: Reference In Queue is null
********************
After GC: Strong Reference is [Ljava.lang.Byte;@74a14482
After GC: Soft Reference is [Ljava.lang.Byte;@1540e19d
After GC: Weak Reference is null
After GC: Phantom Reference is null
After GC: Reference In Queue is java.lang.ref.WeakReference@14ae5a5
After GC: Reference In Queue is java.lang.ref.PhantomReference@7f31245a
After GC: Reference In Queue is null
********************
After OOM: Strong Reference is [Ljava.lang.Byte;@74a14482
After OOM: Soft Reference is null
After OOM: Weak Reference is null
After OOM: Phantom Reference is null
After OOM: Reference In Queue is java.lang.ref.SoftReference@6d6f6e28
After OOM: Reference In Queue is null
  1. 初始状态下,虚引用用就返回null,其余三个引用都有值。
  2. 当触发GC以后,弱引用指向的对象也被回收了,并且能够看到弱引用虚引用两个引用对象被加到了它们相关联的引用队列中了;强引用软引用仍是能够取到值。
  3. 当JVM内存不足以后,软引用也被内存回收了,同时该软引用也被加到了与之关联的引用队列中了。而强引用依然能取到值。

源码解析

如下是引用类的UML图code

Reference UML

弱引用软引用虚引用都继承自Reference类,咱们从Reference类看起htm

Reference类

// 此Reference对象可能会有四种状态:active, pending, enqueued, inactive
// avtive: 新建立的对象状态是active
// pending: 当Reference所指向的对象不可达,而且Reference与一个引用队列关联,那么垃圾收集器
//     会将Reference标记为pending,而且会将之加到pending队列里面
// enqueued: 当Reference从pending队列中,移到引用队列中以后,就是enqueued状态
// inactive: 若是Reference所指向的对象不可达,而且Reference没有与引用队列关联,Reference
//     从引用队列移除以后,变为inactive状态。inactive就是最终状态
public abstract class Reference<t> {
    // 该对象就是Reference所指向的对象,垃圾收集器会对此对象作特殊处理。
    private T referent;         /* Treated specially by GC */
    // Reference相关联的引用队列
    volatile ReferenceQueue<!--? super T--> queue;
    // 当Reference是active时,next为null
    // 当该Reference处于引用队列中时,next指向队列中的下一个Reference
    // 其余状况next指向this,即本身
    // 垃圾收集器只需判断next是否是为null,来看是否须要对此Reference作特殊处理
    volatile Reference next;
    // 当Reference在pending队列中时,该值指向下一个队列中Reference对象
    // 另外垃圾收集器在GC过程当中,也会用此对象作标记
    transient private Reference<t> discovered;  /* used by VM */

    // 锁对象
    static private class Lock { }
    private static Lock lock = new Lock();

    // pending队列,这里的pending是pending链表的队首元素,通常与上面的discovered变量一块儿使用
    private static Reference<object> pending = null;
    // 获取Reference指向的对象。默认返回referent对象
    public T get() {
        return this.referent;
    }
}

Reference类跟垃圾收集器紧密关联,其状态变化以下图所示:对象

Reference State

上述步骤大多数都是由GC线程来完成,其中PendingEnqueued是用户线程来作的。Reference类中定义了一个子类ReferenceHandler,专门用来处理Pending状态的Reference。咱们来看看它具体作了什么。blog

ReferenceHandler类

public abstract class Reference<t> {
    // 静态块,主要逻辑是启动ReferenceHandler线程
    static {
        // 建立ReferenceHandler线程
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());
            Thread handler = new 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);
            }
        });
    }

    // 内部类ReferenceHandler,用来处理Pending状态的Reference
    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);
            }
        }
        // 静态块,确保InterruptedException和Cleaner已经被ClassLoader加载
        // 由于后面会用到这两个类
        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

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

        public void run() {
            // 死循环调用tryHandlePending方法
            while (true) {
                tryHandlePending(true);
            }
        }
    }
}

Reference类在加载进JVM的时候,会启动ReferenceHandler线程,并将它设成最高优先级的守护线程,不断循环调用tryHandlePending方法。 接下来看tryHandlePending方法:

// waitForNotify默认是true。
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<object> r;
        Cleaner c;
        try {
            // 须要在同步块中进行
            synchronized (lock) {
                // 判断pending队列是否为空,pending是队首元素
                if (pending != null) {
                    // 取到pending队列队首元素,赋值给r
                    r = pending;
                    // Cleaner类是Java NIO中专门用来清理堆外内存(DirectByteBufer)的类,这里对它作了特殊处理
                    // 当没有其余引用指向堆外内存时,与之关联的Cleaner会被加到pending队列中
                    // 若是该Reference是Cleaner实例,那么取到该Cleaner,后续能够作一些清理操做。
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // r.discovered就是下一个元素
                    // 如下操做即为将队首元素从pending队列移除
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // 若是pending队列为空,则释放锁等待
                    // 当有Reference添加到pending队列中时,ReferenceHandler线程会今后处被唤醒
                    if (waitForNotify) {
                        lock.wait();
                    }
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // OOM时,让出cpu
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }
        // 给Cleaner的特殊处理,调用clean()方法,以释放与之关联的堆外内存
        if (c != null) {
            c.clean();
            return true;
        }
        // 此处,将此Reference加入到与之关联的引用队列
        ReferenceQueue<!--? super Object--> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

看到这里,豁然开朗。ReferenceHandler线程专门用来处理pending状态的Reference,跟GC线程组成相似生产者消费者的关系。当pending队列为空,则等待;当Reference关联的对象被回收,Reference被加入到pending队列中以后,ReferenceHandler线程会被唤醒来处理pendingReference,主要作三件事:

  1. 将该Referencepending队列移除
  2. 若是该ReferenceCleaner的实例,那么调用clean方法,释放堆外内存
  3. Reference加入到与之关联的引用队列

ReferenceQueue

引用队列比较简单,能够直接理解为一个存放Reference的链表,在此再也不费笔墨。

虚引用PhantomReference

// 灰常简单,只重写了一个构造方法,一个get方法
public class PhantomReference<t> extends Reference<t> {
    // get方法永远返回null
    public T get() {
        return null;
    }

    // 只提供了一个包含ReferenceQueue的构造方法,说明它必须和引用队列一块儿使用
    public PhantomReference(T referent, ReferenceQueue<!--? super T--> q) {
        super(referent, q);
    }
}

通常状况下虚引用使用得比较少,最为人所熟知的就是PhantomReference的子类Cleaner了,它用来清理NIO中的堆外内存。有机会能够专门写篇文章来说讲它。

弱引用WeakReference

// 更加简单,只重写了两个构造方法
public class WeakReference<t> extends Reference<t> {
    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<!--? super T--> q) {
        super(referent, q);
    }
}

太过简单,不作额外讲解。

软引用SoftReference

// 相比WeakReference,它增长了两个时间戳,clock和timestamp
// 这两个参数是实现他们内存回收上区别的关键
public class SoftReference<t> extends Reference<t> {
    // 每次GC以后,若该引用指向的对象没有被回收,则垃圾收集器会将clock更新成当前时间
    static private long clock;
    // 每次调用get方法的时候,会更新该时间戳为clock值
    // 因此该值保存的是上一次(最近一次)GC的时间戳
    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;
    }
    // 每次调用,更新timestamp的值,使之等于clock的值,即最近一次gc的时间
    public T get() {
        T o = super.get();
        if (o != null &amp;&amp; this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}

SoftReference除了多了两个时间戳以外,跟WeakReference几乎没有区别,它是如何作到在内存不足时被回收这件事的呢?其实这是垃圾收集器干的活。垃圾收集器回收SoftReference所指向的对象,会看两个维度:

  1. SoftReference.timestamp有多老(距上一次GC过了多久)
  2. JVM的堆空闲空间有多大

而具体何时回收SoftReference所指向的对象呢,能够参考以下公式: > interval <= free_heap * ms_per_mb

其中interval为上一次GC与当前时间的差值,以毫秒为单位;free_heap为当前JVM中剩余的堆空间大小,以MB为单位;ms_per_mb能够理解为一个常数,即每兆空闲空间可维持的SoftReference的对象生存的时长,默认为1000,能够经过JVM参数*-XX:SoftRefLRUPolicyMSPerMB设置。 若是上述表达式返回true,则清理SoftReference所指向的对象,并将该SoftReference加入到pending*队列中;不然不作处理。因此说在JVM内存不足的时候回收软引用这个说法不是很是准确,只是个经验说法,软引用的回收,还跟它存活的时间有关,甚至跟JVM参数设置(-XX:SoftRefLRUPolicyMSPerMB)都有关系!

参考

How Hotspot Clear Softreference</t></t></t></t></t></t></object></t></object></t></t></string></byte[]></byte[]></byte[]>

相关文章
相关标签/搜索