一、前言
JDK1.2以前只提供一种引用类型——强引用 Object obj = new Object(); 。而JDK1.2后我们多另外的三个选择分别是软引用 java.lang.ref.SoftReference 、弱引用 java.lang.ref.WeakReference 和虚引用 java.lang.ref.PhantomReference 。下面将记录对它们和相关连的引用队列 java.lang.ref.ReferenceQueue 和 java.util.WeakHashMap 的学习笔记。
二、四种引用类型
1. 强引用(Strong Reference)
最常用的引用类型,如Object obj = new Object();。只要强引用存在则GC时则必定不被回收。
2. 软引用(Soft Reference)
用于描述还游泳但非必须的对象,当堆将发生OOM(Out Of Memory)时则会回收软引用所指向的内存空间,若回收后依然空间不足才会抛出OOM。
一般用于实现内存敏感的高速缓存。
示例:实现学生信息查询操作时有两套数据操作的方案
一、将得到的信息存放在内存中,后续查询则直接读取内存信息;(优点:读取速度快;缺点:内存空间一直被占,若资源访问量不高,则浪费内存空间)
二、每次查询均从数据库读取,然后填充到TO返回。(优点:内存空间将被GC回收,不会一直被占用;缺点:在GC发生之前已有的TO依然存在,但还是执行了一次数据库查询,浪费IO)
通过软引用解决:
ReferenceQueue q = newReferenceQueue();//获取数据并缓存
Object obj = newObject();
SoftReference sr= newSoftReference(obj, q);//下次使用时
Object obj = (Object)sr.get();if (obj == null){//当软引用被回收后才重新获取
obj = newObject();
}//清理被收回后剩下来的软引用对象
SoftReference ref = null;while((ref = q.poll()) != null){//清理工作
}
3. 弱引用(Weak Reference)
发生GC时必定回收弱引用指向的内存空间。
4. 虚引用(Phantom Reference)
又称为幽灵引用或幻影引用,,虚引用既不会影响对象的生命周期,也无法通过虚引用来获取对象实例,仅用于在发生GC时接收一个系统通知。
那现在问题来了,若一个对象的引用类型有多个,那到底如何判断它的可达性呢?其实规则如下:
1. 单条引用链的可达性以最弱的一个引用类型来决定;
2. 多条引用链的可达性以最强的一个引用类型来决定;
我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可及对象(当将要发生OOM时则会被回收掉)。
软引用、弱引用和虚引用均为抽象类 java.lang.ref.Reference 的子类,而与引用队列和GC相关的操作大多在抽象类Reference中实现。
三、引用队列(java.lang.ref.ReferenceQueue)
引用队列配合Reference的子类等使用,当引用对象所指向的内存空间被GC回收后,该引用对象则被追加到引用队列的末尾(源码中 boolean enqueue(Reference extends T> r) { /*Called only by Reference class*/ 说明只供Reference实例调用,且仅能调用一次)。引用队列有如下实例方法:
Reference extends T> ReferenceQueue#poll() ,从队列中出队一个元素,若队列为空则返回null。
Reference extends T> ReferenceQueue#remove() ,从队列中出队一个元素,若没有则阻塞直到有元素可出队。
Reference extends T> ReferenceQueue#remove(long timeout) ,从队列中出队一个元素,若没有则阻塞直到有元素可出队或超过timeout指定的毫秒数(由于采用wait(long timeout)方式实现等待,因此时间不能保证)。
四、 java.lang.ref.Reference
Reference内部通过一个 {Reference} next 的字段来构建一个Reference类型的单向链表。另外其内部还包含一个 ReferenceQueue super T> queue 字段存放引用对象对应的引用队列,若Reference子类构造函数中没有指定则使用ReferenceQueue.NULL,也就是说每个软、弱、虚引用对象必定与一个引用队列关联。
Reference还包含一个静态字段 {Reference} pending (默认为null),用于存放被GC回收了内存空间的引用对象单向链表。Reference通过静态代码块启动一个优先级最高的守护线程检查pending字段为null,若不为null则沿着单向链表将引用对象追加到该引用对象关联的引用队列当中(除非引用队列为ReferenceQueue.NULL)。守护线程的源码如下:
public voidrun() {for(;;) {
Reference r;
synchronized (lock) {
// 检查pending是否为nullif (pending != null) {
r=pending;
Reference rn=r.next;
pending= (rn == r) ? null: rn;
r.next=r;
}else{try{
// pending为null时,则将当前线程进入wait set,等待GC执行后执行notifyAlllock.wait();
}catch(InterruptedException x) { }continue;
}
}//Fast path for cleaners
if(r instanceof Cleaner) {
((Cleaner)r).clean();continue;
}
// 追加到对应的引用队列中
ReferenceQueue q=r.queue;if (q !=ReferenceQueue.NULL) q.enqueue(r);
}
}
注意:由于通过静态代码块进行线程的创建和启动,因此Reference的所有子类实例均通过同一个线程进行向各自的引用队列追加引用对象的操作。
五、java.util.WeakHashMap
由于WeakHashMap的键对象为弱引用,因此当发生GC时键对象所指向的内存空间将被回收,被回收后再调用size、clear或put等直接或间接调用私有expungeStaleEntries方法的实例方法时,则这些键对象已被回收的项目(Entry)将被移除出键值对集合中。
下列代码将发生OOM
public static voidmain(String[] args) throws Exception {
List> maps = new ArrayList>();for (int i = 0; i < 1000; i++) {
WeakHashMap d = new WeakHashMap();
d.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(d);
System.gc();
System.err.println(i);
}
}
而下面的代码因为集合的Entry被移除因此不会发生OOM
public static voidmain(String[] args) throws Exception {
List> maps = new ArrayList>();for (int i = 0; i < 1000; i++) {
WeakHashMap d = new WeakHashMap();
d.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(d);
System.gc();
System.err.println(i);for (int j = 0; j < i; j++) {
// 触发移除Entry操作
System.err.println(j+ "size" + maps.get(j).size());
}
}
}
六、总结
七、参考
《WeakHashMap的神话》http://www.javaeye.com/topic/587995
http://hongjiang.info/java-referencequeue/