什么是强引用、软引用、弱引用、虚引用以及四种引用的区别及其实现

在JDK1.2以前引用的传统定义: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。但是这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。不具备对于对象状态的描述。 

所以在之后Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

通过四种引用能让对象在不同状态接受不同的操作: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。 

四种引用都继承于Reference类,以下是Reference类的源码:

    public abstract class Reference<T> {

        private static boolean disableIntrinsic = false;

        private static boolean slowPathEnabled = false;

        //保存reference指向的对象

volatile T referent;

//引用对象关联的引用队列

final ReferenceQueue<? super T> queue;

//指向下一个引用,Reference是一个单链表的结构

Reference queueNext;

Reference<?> pendingNext;

public T get() {

        return getReferent();

}

@FastNative

private final native T getReferent();

public void clear() {

    clearReferent();

}

@FastNative

native void clearReferent();

public boolean isEnqueued() {

    return queue != null && queue.isEnqueued(this);

}

        public boolean enqueue() {

    return queue != null && queue.enqueue(this);

}

/* --Reference类中有两个构造函数,一个需要传入引用队列,另一个则不需要。 -- */

Reference(T referent) {

    this(referent, null);

}

Reference(T referent, ReferenceQueue<? super T> queue) {

    this.referent = referent;

    this.queue = queue;

}

    }

这个队列的意义在于增加一种判断机制,可以在外部通过该队列来判断对象是否被回收。如果一个对象即将被回收,那么引用这个对象的reference对象就会被放到这个队列中,然后外部程序即可通过监控该队列就可以取出reference再进行一些处理。这里的ReferenceQueue名义上是一个队列,实际内部是使用单链表来表示的单向队列,可以理解为queue就是一个链表,其自身仅存储当前的head节点,后面的节点由每个reference节点通过next来保持即可。

如果没有这个队列,就只能通过不断地轮询reference对象,通过get方法是否返回null,但是phantomReference对象不具备这种能力,其get方法始终返回null,所以它只能通过队列来判断对象是否被回收。

这两种方法均有相应的使用场景,具体使用需要具体情况具体分析。比如在weakHashMap中,就通过查询queue的数据,来判定是否有对象将被回收。而ThreadLocalMap,则采用判断get()是否为null来进行处理。

其中Reference的很多方法涉及到了ReferenceQueue的几个方法,包括enqueue(Reference)、enqueueLocked(Reference)、isEnqueued(),以下是源码:

    boolean enqueue(Reference<? extends T> reference) {

//可以看到首先获取同步锁,然后调用了enqueueLocked(Reference)

synchronized (lock) {

    if (enqueueLocked(reference)) {

        lock.notifyAll();

        return true;

    }

return false;

}

    }

    private boolean enqueueLocked(Reference<? extends T> r) {

if (r.queueNext != null) {

    return false;

}

if (r instanceof Cleaner) {

    Cleaner cl = (sun.misc.Cleaner) r;

    cl.clean();

    r.queueNext = sQueueNextUnenqueued;

    return true;

}

if (tail == null) {

    head = r;

} else {

    tail.queueNext = r;

}

tail = r;

tail.queueNext = r;

return true;

    }

ReferenceQueue通过 enqueueLocked函数维护了一个队列,该队列具有链表结构,而enqueue这一系列函数就是将reference添加到这个链表中。

reference.queueNext != sQueueNextUnenqueued”用于判断该Reference是否是一个Cleaner类,在上面ReferenceQueue的enqueueLocked函数中我们可以看到如果一个Reference是一个Cleaner,则调用它的clean方法,同时并不加入链表,并且将其queueNext设置为sQueueNextUnequeued,这是一个空的虚引用

关于Cleaner,sun.misc.Cleaner是JDK内部提供的用来释放非堆内存资源的API。JVM只会帮我们自动释放堆内存资源,但是它提供了回调机制,通过这个类能方便的释放系统的其他资源。Cleaner是用于释放非堆内存的,所以做特殊处理。

boolean isEnqueued(Reference<? extends T> reference) {

    synchronized (lock) {

    return reference.queueNext != null && reference.queueNext != sQueueNextUnenqueued;

    }

}

先获取同步锁,然后判断该reference是否在队列中。由于enqueue和isEnqueue函数都要申请同步锁,所以这是线程安全的。

通过enqueue和isEnqueue两个函数的分析,ReferenceQueue队列维护了那些被回收对象referent的Reference的引用,这样通过isEnqueue就可以判断对象referent是否已经被回收,用于一些情况的处理。

⑴强引用(StrongReference)

强引用可以直接访问目标对象是使用最普遍的引用,如果一个对象具有强引用,那垃圾回收器就不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会回收具有强引用的对象来解决内存不足的问题。  

使用场景:平时A a = new A()

⑵软引用(SoftReference)

用来描述还有用但是并非必须的对象,如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足要发生溢出前,就会把这些对象列入回收范围中进行二次回收。只要垃圾回收器没有回收它,该对象就可以被程序使用。清理是由垃圾收集线程根据其特定算法按照内存需求决定的,也可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。

提供了SoftReference类来实现软引用,它的特点是一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

但是需要注意的是,被软引用对象关联的对象会自动被垃圾回收器回收,但软引用对象本身也是一个对象,创建的软引用并不会自动被垃圾回收器回收掉,所以需要手动去清理软引用本身,否则也会导致OOM的产生,合适的处理方式是注册一个引用队列,将软引用和一个ReferenceQueue联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,每次循环之后将引用队列中出现的软引用对象从cache中移除。

Object obj = new Object();

SoftReference<Object> sf = new SoftReference<Object>(obj);

obj = null;

sf.get();//有时候会返回null

使用场景:创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。

以下是软引用的源码:

public class SoftReference<T> extends Reference<T> {

    //由垃圾回收器负责更新的时间戳,在JVM初始化时会对变量clock进行初始化
    //在JVM发生GC时,也会更新clock的值,所以clock会记录上次GC发生的时间点 
    static private long clock;

    //timestamp是在创建和更新时的时间戳
    //在创建和get方法调用时将其更新为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; 
    } 

    //返回引用指向的对象,如果referent已经被程序或者垃圾回收器清理,则返回null。 
    public T get() { 
        T o = super.get(); 
        if (o != null && this.timestamp != clock) 
        this.timestamp = clock; 
        return o; 
    } 
}

软引用是当内存不足时可以回收的,但是回收时并不会一次性全部回收,软应用的回收有一个条件,常用的回收策略是基于当前堆大小的LRU策略,会使用clock的值减去timestamp,得到的差值就是这个软引用被闲置的时间,如果闲置足够时间就认为是可被回收的:

clock - timestamp <= free_heap * ms_per_mb

clock:记录了上一次GC的时间。这个变量由GC(garbage collector)来改变。

timestamp:记录对象被访问(get函数)时最近一次GC的时间。

free_heap:JVM 堆的空闲大小,单位是MB

ms_per_mb:单位是毫秒,是每MB空闲允许保留软引用的时间,默认的生存周期为1000ms/Mb。Sun JVM可以通过参数-XX:SoftRefLRUPolicyMSPerMB进行设置

如果一个对象A仅剩下一个软引用在引用它,目前有3MB的空闲,ms_per_mb为1000,这时如果clock和timestamp分别为5000和2000,那么5000 - 2000 <= 3 * 1000条件成立,则该次GC不对该软引用进行回收。所以每次GC时,通过上面的条件去判断内存是否不足,软应用是否需要被回收。

⑶弱引用(WeakReference)

弱引用也是用来描述非必须对象的,弱引用与软引用的区别在于:强度要更弱,只具有弱引用的对象拥有更短暂的生命周期,与弱引用关联的对象只能生存到最近的下次GC,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。通过WeakReference类来实现弱引用。

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

Object obj = new Object();

WeakReference<Object> wf = new WeakReference<Object>(obj);

obj = null;

wf.get();//有时候会返回null

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

使用场景:Java源码中的java.util.WeakHashMap中的key就是使用弱引用,一旦使用过后不在需要某个引用,JVM会自动处理它,就不需要使用者做其它操作。而WeakHashMap也内置了一个ReferenceQueue,来获取键对象的引用情况。

以下是弱引用的源码:

public class WeakReference<T> extends Reference<T> { 
    //构造函数的使用情况与软引用类似,都是基于父类Reference完成的,没有其他代码,GC时会被回收掉。
    public WeakReference(T referent) { 
        super(referent);

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

⑷虚引用(PhantomReference)

虚引用也称为幽灵引用,它是最弱的一种引用关系,与其他几种引用都不同,虚引用并不会决定对象的生命周期,也无法通过虚引用来取得对象的实例。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的过程,当对象和虚引用关联起来之后,当这个对象被收集器回收是就会被送到引用队列中,这样当检测到队列中收到一个引用的时候就意味着对象被回收了,可以采取一些行动。通过PhantomReference类实现虚引用。

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

PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());

prf.get();//永远返回null

prf.isEnQueued();//返回是否从内存中已经删除

使用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效 

以下是虚引用的源码:

public class PhantomReference<T> extends Reference<T> { 

    public T get() { 
        return null; 
    } 

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

这里的get()返回的是null,这就是虚引用无法获取对象引用的原因,所以并不是虚引用不持有对象的引用,通过构造函数可以看到虚引用是持有对象引用的,但是被限制只能返回null。

同时虚引用只有一个构造函数,所以必须传入ReferenceQueue对象。虚引用的作用是判断对象是否被回收,这个功能是通过ReferenceQueue实现的,但不仅仅是虚引用可以判断回收,弱引用和软引用同样可以通过具有ReferenceQueue的构造函数来判断是否被回收。

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/105393625