详解Java中的四种引用及其应用

本文参考资源:

引用的抽象父类——Reference

Reference的状态

在Reference源码开头的一段注释中谈到了:

一个引用的实例处于四种状态之一:
+ Active:新创建的实例为Active状态,当处于这个状态一段时间后状态变为Pending或Inactive,这取决于它创建的时候是否指定了ReferenceQueue。
+ Pending:当Reference包装的referent = null的时候,JVM会把Reference设置成pending状态。如果Reference创建时指定了ReferenceQueue,那么会被ReferenceHandler线程处理进入到ReferenceQueue队列中,如果没有就进入Inactive状态。
+ Enqueue: 进入ReferenceQueue中的对象,等待被回收
+ Inactive: Reference对象从ReferenceQueue取出来并被处理掉。处于Inactive的Reference对象状态不能再改变

核心成员变量

1) referent: 表示被包装的对象

下面代码中new Object()就是被包装的对象。

WeakReference<Object> wo = new WeakReference<Object>(new Object());

2) queue: 表示被包装的对象被回收时,需要被通知的队列,该队列在Reference构造函数中指定。当referent被回收的时候,Reference对象就处在了Pending状态,Reference会被放入到该队列中,如果构造函数没有指定队列,那么就进入Inactive状态。

3) pending: 表示等待被加入到queue的Reference 列表。

private static Reference<Object> pending = null;

pending理解链表有点费解,因为代码层面上看这明明就是Reference对象。其实当Reference处在Pending状态时,他的pending字段被赋值成了下一个要处理的对象(即下面讲的discovered),通过discovered可以拿到下一个对象并且赋值给pending,直到最后一个,所以这里就可以把它当成一个链表。而discovered是JVM的垃圾回收器添加进去的,大家可以不用关心底层细节。

4) discovered: 当处于Reference处在pending状态:discovered为pending集合中的下一个元素;其他状态:discovered为null

transient private Reference<T> discovered;  /* used by VM */

5) next: 当Reference对象在queue中时(即Reference处于Enqueued状态),next描述当前引用节点所存储的下一个即将被处理的节点。

@SuppressWarnings("rawtypes")
Reference next;

ReferenceHandler线程会把pending状态的Reference放入ReferenceQueue中,上面说的next,discovered 字段在入队之后也会发生变化,下一小节会介绍。

ReferenceQueue入队过程

我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
        // Check that since getting the lock this reference hasn't already been
        // enqueued (and even then removed)
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        //设置queue状态
        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节点r进入队列,Reference节点被放在队列头,所以这是一个先进后出队列。 入队的示意图如下:

详解Java中的四种引用及其应用

ReferenceHandler线程

Reference类中另一个比较重要的成员是ReferenceHandler。ReferenceHandler是一个线程。当JVM加载Reference的时候,就会启动这个线程。用jstack查看该线程栈可以看到。Reference Handler是JVM中的2号线程,并且线性优先级被设置为高优先级。

看源代码他是如何工作的:

private static class ReferenceHandler extends Thread {

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

    public void run() {
        for (;;) {
            Reference<Object> r;
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    try {
                        try {
                            lock.wait();
                        } catch (OutOfMemoryError x) { }
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // Fast path for cleaners
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }
            ReferenceQueue<Object> q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}

通过上面代码可以看到ReferenceHandler线程做的是不断的检查pending是否为null, 如果不为null,将pending对象进行入队操作,而pending的赋值由JVM操作。所以ReferenceQueue在这里作为JVM与上层Reference对象管理之间的消息传递方式。

Java中的四种引用

强引用

强引用是我们在代码中最普通的引用,只要维持了引用就不会被回收。

Object o = new Object();   //  强引用  

软引用

SoftReference<String> sr = new SoftReference<String>(new String("hello"));

如果一个对象有一个软引用,那么在内存足够的情况下,该对象就不会被垃圾回收器回收。网上有很多资料说软引用只会在内存空间不够用的情况下对象才会被回收。 那么什么时候才是内存不够用呢?

首先看一下SoftReference类的源码可以看到有两个字段。这两个字段的作用已经标注,这与JVM GC有什么关系呢?

/**
* 记录最近一次被GC的时间。
*/
static private long clock;

/**
* 每次调用get方法的时候更新
* 记录当前Reference最近一次被访问的时间
*/
private long timestamp;

一起看一下HotSpot的源码,对于软引用的回收策略见下面should_clear_reference函数。

// The oop passed in is the SoftReference object, and not
// the object the SoftReference points to.
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                             jlong timestamp_clock) {
    jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
    assert(interval >= 0, "Sanity check");

    // The interval will be zero if the ref was accessed since the last scavenge/gc.
    if(interval <= _max_interval) {
        return false;
    }

    return true;
}

上述代码中interval表示当前引用存活了多久。他的值就是对应上述java代码中的clocktimestamp相减。interval_max_interval比较,如果大于 _max_interval,那么就和弱引用一样处理,如果小于就当做强引用处理。_max_interval的赋值函数如下:

// Capture state (of-the-VM) information needed to evaluate the policy
void LRUMaxHeapPolicy::setup() {
    size_t max_heap = MaxHeapSize;
    max_heap -= Universe::get_heap_used_at_last_gc();
    max_heap /= M;

    _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
    assert(_max_interval >= 0,"Sanity check");
}

通过源码可见首先是max_heap减去上次GC之后剩余堆大小,如果上次GC之后还有很多剩余空间,说明内存空间不够用了,那么max_heap的值就越小,相应_max_interval也越小,软引用就越可能被回收。

软引用的一个作用是实现内存敏感的高速缓存。比如浏览器的后退按钮:
1. 如果网页浏览结束就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建。
2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
通过软引用可以解决该问题

弱引用

WeakReference<String> wr = new WeakReference(new String("123"));

只具有弱引用的对象生命周期更短。当垃圾回收器发现了只有弱引用的对象时候,无论内存空间是否足够,都会被GC回收。当你偶尔需要引用某个对象,随时能获取该对象,但是不想介入该对象的生命周期的时候,就可以使用弱引用, 因为弱引用不会对对象的垃圾回收判断产生附加的影响。

在将引用添加到引用队列时,如果先扫描到对象的弱引用,消极地将弱引用直接加入引用队列,在扫描完所有引用后,若该对象还存活,则将该引用从引用队列中移除。

ThreadLocal中就是使用了弱引用来避免内存内存泄漏,参考:ThreadLocal源码分析和相关理解

虚引用

虚引用不会对对象的垃圾回收有任何附加影响,他与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。查看他的构造方法可以看到必须与一个ReferenceQueue绑定:

public PhantomReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}

jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

比如nio中的DirectByteBuffer,我们在使用ByteBuffer.allocate(int capacity)的时候分配的是堆上的空间,而ByteBuffer.allocateDirect(int capacity)分配的是堆外内存(unsafe.allocateMemory),获得一块DirectByteBuffer之后会创建一个Cleaner实例,而Cleaner就是PhantomReference的子类,Cleaner就起到了跟踪DirectByteBuffer的垃圾回收过程的作用。一旦Cleaner被放入了ReferenceQueue,就会调用内部的clean()方法来回收对应的堆外内存。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e8%af%a6%e8%a7%a3java%e4%b8%ad%e7%9a%84%e5%9b%9b%e7%a7%8d%e5%bc%95%e7%94%a8%e5%8f%8a%e5%85%b6%e5%ba%94%e7%94%a8/

发表评论

电子邮件地址不会被公开。