基础-WeakReference
一、概述
为了更好的理解WeakHashMap的原理,我们有必要先来了解一下WeakReference的作用及实现原理。Java中有一个专门的包java.lang.ref,里面定义了我们通常所说的几种引用,具体来说如下:
Reference: 基础的引用类,是一个抽象类,定义了引用的一些基本方法
SoftReference: 软引用,软引用对象在应用出现OOM之前会被回收。
WeakReference: 弱引用,如果一个对象只被弱引用关联,则垃圾回收器会回收它。
PhantomReference:幻引用,这类用得比较少,个人也不太理解,应该是属于引用级别最弱的引用。
ReferenceQueue: 引用队列,垃圾回收器会将已回收的队象放到这个队列里。
除了上述的三类引用之外,还有一类引用叫做强引用,强引用就是我们通常所说的引用,所以这里java并没有单独定义一个引用类来表示。
本文的重点是介绍WeakReference的使用,及其实现原理。
二、WeakReference的使用示例
在介绍其实现原理前,我们先来看一下它的使用效果,根据WeakReference的定义, 如果一个对象只被弱引用所引用,那么这个对象就是弱可达的,弱可达对象会在系统进行垃圾回收时被回收。
可能这样说还是不好理解,所以我们以一个示例来说明其使用的效果,代码如下:
public class TestWeakReference { public static void main(String[] args) throws Exception{ UserInfo userInfo = new UserInfo("Jim Green"); UserInfo anotherUser = userInfo; WeakReference<UserInfo> weakUser = new WeakReference<>(userInfo); System.out.println("\nBefore userInfo is null"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); userInfo = null; System.gc(); System.out.println("\nAfter userInfo is null"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); anotherUser = new UserInfo("Jim White"); System.gc(); System.out.println("\nAfter anotherUser is changed"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); } } class UserInfo { private String name; public UserInfo(String name){ this.name = name; } @Override public String toString() { return "Name is " + name; } }
上述代码的运行结果如下:
从上面的结果可以看到,刚开始,这三个引用都指向同一个用户对象Jim Green, 然后我们将原引用置为null,进行垃圾回收,但由于还有一个强引用指向这个Jim Green, 所以这个用户对象不会被回收,最后,当没有强引用再指向它了,再做垃圾回收,则Jim Green被回收了。
这个结果也印证了WeakReference的作用,如果其指向的对象没有被任何强引用指向,则该对象是可以回收的。
下面我们再以图例的形式来演示这个过程。
三、WeakReference 的实现原理
上面我们详细演示了使用的方式,接下来我们再说一下其主要实现。
1. UML图
WeakReference , Reference和ReferenceQueue三者之间的关系如下:
上图中列出了这三个类的一些主要的属性和方法。其中WeakReference是抽象类,但是提供了一个引用所需要的基本功能,而WeakReference则只是简单继承,并没有实现任何扩展的功能。
Reference有两个构造的方法,分别是带队列和不带队列的,如下:
Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
可见,referent属性存储了其所引用的对象,而queue这个字段是可选的,前面说到,queue的作用保存对象将被回收的引用,由垃圾回收器负责往里面添加,但如果不提供,则没有这一过程。
2. 状态说明:
根据类的说明,引用有四种状态:
Active: 这是创建该引用后的初始状态,如果该引用对象所引用的对象的可达性发生了变化,则引用本身的状态将变为Pending或Inactive,取决于这个引用在创建时是否使用了队列。如果使用了队列,则状态将会转化为Pending,否则直接变为Inactive。
Pending: 由Active转化而来,处于该状态的引用在一个公共的pending队列里,等待被添加到queue里去。对于pending队列的处理由一个优先级比较高的守护线程实时监控处理。如果该引用在初始化时没有使用queue,则不会被加到pending队列里,当然也不可能添加到queue里。
Enqueued: 由Pending转化而来,如果pending列表中的引用被正确添加到了queue里,则引用的状态为enqueued,如果该引用被从queue队列里移除了,则其将变为InActive状态。同样,这个前提就是构造时需要指定queue.
InActive: 最后的状态,处于这个状态的引用对象实际上已经没什么作用了,状态也不会发生任何改变。
这四种状态只是一种说明,实际上Reference对象并没有任何的status字段,不过作为队列中的节点,它有一个next字段,当状态为Active时,其next为null,而当其为其它状态时,next一定不为null,而是指向队列中的下一个引用,如果其本身就是队列中的最后一个元素,则next指向其自身。
3. 原理说明:
在介绍了结构和状态说明后,我们再来对其实现进行分析,这个需要分两种情况:
1)构造时没有使用queue:
这种情况比较简单,状态转换只有Active和InActive,不涉及到队列的操作,当引用所指向的对象没有任何其它的强引用时,垃圾回收器将会回收该对象,而其状态也应该会变为InActive.值得注意的时,这样get()就会直接返回null了。
2)构造函数使用了queue:
这种情况就复杂了,当引用指向的对象没有其它的强引用时,垃圾回收器会先将其添加到pending队列里,而Reference会通过一个公共的守护线程来处理pending队列里的引用对象,将其添加到queue队列中去。
这个过程的处理逻辑如下:
1 private static class ReferenceHandler extends Thread { 2 3 ReferenceHandler(ThreadGroup g, String name) { 4 super(g, name); 5 } 6 7 public void run() { 8 for (;;) { 9 10 Reference r; 11 synchronized (lock) { 12 if (pending != null) { 13 r = pending; //取一个元素 14 Reference rn = r.next;//找队列中下一个引用对象 15 pending = (rn == r) ? null : rn; //如果是最后一个元素,则pending队列置空 16 r.next = r; //改变其next 17 } else { 18 try { 19 lock.wait(); //等待 20 } catch (InterruptedException x) { } 21 continue; 22 } 23 } 24 25 // Fast path for cleaners 26 if (r instanceof Cleaner) { 27 ((Cleaner)r).clean(); 28 continue; 29 } 30 31 ReferenceQueue q = r.queue; 32 if (q != ReferenceQueue.NULL) q.enqueue(r);//调用队列中的入队方法 33 } 34 } 35 }
上面的方法会一直处理pending队列直到为null,之后将处于wait状态,那么问题来了,当pending队列再次不为空时,这个线程需要被唤醒。往pending队列里加引用对象,并执行唤醒操作的工作是谁来完成的呢?答案是由垃圾回收器在回收引用指向的对象时来调用的。
所以说,是垃圾回收器完成了引用对象从Active到Pending的转换,而引用对象的线程完成了引用对象由Pending到Enqueued的转换。
接下来我们再了解下ReferenceQueue中入队的处理过程:
1 boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */ 2 synchronized (r) { 3 if (r.queue == ENQUEUED) return false; 4 synchronized (lock) { 5 r.queue = ENQUEUED; 6 r.next = (head == null) ? r : head; 7 head = r; 8 queueLength++; 9 if (r instanceof FinalReference) { 10 sun.misc.VM.addFinalRefCount(1); 11 } 12 lock.notifyAll(); 13 return true; 14 } 15 } 16 }
从其实现逻辑可以看出,每次入队是从头入队,入队后,更新其queue属性,这样可以防止多次入队。
对应还有出队的功能,这个就不再分析了。
queue的入队我们介绍完了,但是垃圾回收器不会对这个队列做出队操作,那么这个队列有什么用呢?JDK中有一段对其的描述如下:
“在创建引用对象时,通过向 引用队列 注册 一个适当的引用对象,程序可以请求在对象可到达性更改时获得通知。在垃圾回收器确定引用的可到达性已经更改为对应于引用类型的值之后的某一时间,它会将引用添加到相关的队列中。此时,该引用被认为是 已加入队列的。通过轮询或阻塞,直到获得了引用,程序才可以从队列中移除引用。引用队列是通过
类实现的。”ReferenceQueue
所以,这个queue是提供给应用程序通知用的,也就是说,程序可以通过监听这个队列,来获悉哪些弱引用所指向的对象已经被回收了,进而程序可以做相应的处理。至于如何监控,可以参考Reference类中对于pending队列的处理方式。
四、总结
至此,我们对Reference及ReferenceQueue的实现方式做了一个完整的介绍,下面再总结一下:
1. WeakReference在创建时需要关联到另一个对象,如果该对象没有别的普通(强)引用,则该对象将会被垃圾回收器回收。
2. Reference在定义时可以指定一个类型为ReferenceQueue的队列,该队列的作用则是存储那些关联对象已经被回收了的Reference对象,以供应用程序监听,但如何处理由应用程序自己决定。
3. WeakReference继承于Reference,但自身并未提供扩展的功能。