本文中源码来自JDK 8
Java中有4种引用类型,帮助JVM更好地进行内存管理。深入理解Reference,是学习WeakHashMap、ThreadLocal、缓存组件等的基础。本文将带你全面学习Reference体系实现。
1 Java中的引用类型
在Java中,为了更好地进行内存管理,对一个类对象,可以创建它的多种引用类型:强引用、软引用、弱引用和虚引用。
// 强引用
Object obj = new Object();
// 软引用
SoftReference<Object> softRef = new SoftReference<>(obj);
// 弱引用
WeakReference<Object> weakRef = new WeakReference<>(obj);
// 虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
1.1 强引用
Strong Reference,是Java中最为普遍的引用类型。当一个对象存在强引用时,垃圾回收器不会回收这个对象,即使系统内存不足。只有它的强引用被显式释放时,该对象才会成为垃圾回收的候选对象。
// 强引用
Object obj = new Object();
// 释放强引用,可被垃圾回收
obj = null
1.2 软引用
Soft Reference,用于描述还有用但并非必须的对象,在内存不足时可以被垃圾回收。
例如,程序中的大对象,业务上的生命周期很长,又能够方便地重建该对象:
- 如果仅使用它的强引用,将导致长期占用程序内存;
- 如果使用软引用,允许内存不足时将它回收;
- 当再次使用它时,已不能从软引用中获得,只需重建该对象即可。
可见,软引用将允许程序OOM前进行自救(回收软引用后可能会避免OOM);也是一种用时间(重建对象的耗时)换取空间(回收软引用的内存)的策略。
某些程序缓存就可使用软引用。
// 20MB 大对象
public static SoftReference<byte[]> softRef = new SoftReference<>(new byte[20 * 1024 * 1024]);
public static void softRef() {
byte[] array = softRef.get();
if (array == null) {
// 已被回收,重建
array = new byte[10 * 1024 * 1024];
softRef = new SoftReference<>(array);
}
// 使用array,array的强引用将在当前方法退出时被释放
doSomething(array);
}
1.3 弱引用
Weak Reference,当一个对象只被弱引用关联时,即使内存并不紧张,在下一次垃圾回收时,该对象都就有可能被回收。
- 如果在创建弱引用指定引用队列,弱引用对象被回收时,会把该对象放入引用队列中;
- 从弱引用中获取对象,每次都要判断一下是否为空,来避免空指针异常
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);
// 手动触发gc
System.gc();
Object obj = weakRef.get();
// 已被回收
if (obj == null) {
Reference<?> poll = queue.remove();
System.out.println(poll);
System.out.println(weakRef);
System.out.println(poll == weakRef);
}
// 没被回收时,正常使用obj
1.4 虚引用
Phantom Reference,主要用于跟踪对象被垃圾回收的状态,无法通过该引用直接获取到对象实例。垃圾回收器随时可能回收只有被虚引用关联的对象。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 输出null
System.out.println( phantomRef.get());
System.gc();
Reference<?> ref = queue.remove();
// 输出 true
System.out.println(ref == phantomRef);
2 Reference抽象类
强引用无需包装,其他引用类型都是Reference的子类。
Reference关键属性有:
// 关联的对象
private T referent;
// 引用队列
volatile ReferenceQueue<? super T> queue;
// 在队列中的下一个引用,如果是最后一个引用,则nest指向自己
Reference next;
// 静态属性,充当入队操作的synchronized锁对象
private static Lock lock = new Lock();
// 静态属性,待入队引用
private static Reference<Object> pending = null;
// 成员属性,下一个待入队元素
transient private Reference<T> discovered;
2.1 待入队队列
对于static属性pending,源码中有如下解释:
等待进入队列的引用列表。垃圾收集器将引用添加到此列表中,而引用处理程序线程将它们删除。该列表受上述锁对象的保护。列表使用discovered属性来链接其中的元素。
可见,待入队引用,由JVM的垃圾回收器加入到pending队列中,队列内通过discovered相关联,pending指向头元素,先进先出。
pending列表会被
入队线程消费。
2.2 入队线程
Reference中嵌套类ReferenceHandler,是Thread的子类,在static块中创建了它的实例,最高优先级、守护模式,同时启动它。
ReferenceHandler的run方法如下,主要逻辑有:
- pending为null时,入队线程将wait阻塞;
- 当JVM回收一个软引用、弱引用或虚引用(称为ref)时,将pending指向该ref,并调用notify()唤醒入队线程;
- 将pending指向ref.discovered,调用ReferenceQueue.enqueue()将ref入队;
- 进入下一次for循环。
public void run() {
for (;;) {
// 本次要入队的引用
Reference<Object> r;
// 防止并发
synchronized (lock) {
if (pending != null) {
// 将pending入队
r = pending;
// 指向下一个待入队引用
pending = r.discovered;
//
r.discovered = null;
} else {
// 阻塞等待下一个要入队的引用
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// 入队
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
可见,ReferenceHandler就是一个消费者(生产者是JVM垃圾回收器),作用是将pending队列中引用异步放入ReferenceQueue;
3 ReferenceQueue的作用
从Java1.2起,源码中介绍如下:
Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
引用队列,在检测到适当的可达性更改后,将垃圾收集器将已注册的引用对象追加到该队列。
一个弱引用(或软引用、虚引用)何时被gc回收,不受用户线程控制。当一个弱引用(或软引用、虚引用)关联的对象被gc回收时,用户线程希望得到通知,进行一些额外处理,如数据清理等。目前有两种方式:
- 可以通过
weakRef.get()是否返回null来判断; - 使用ReferenceQueue,对象被gc回收时,JVM会将相应的引用放入ReferenceQueue中。
可见,ReferenceQueue是引用关联的对象被gc回收时,JVM给用户线程的通知队列。
3.1 主要属性
// 一个标识,代表不能入队
static ReferenceQueue<Object> NULL = new Null<>();
// 一个标识,代表已入队
static ReferenceQueue<Object> ENQUEUED = new Null<>();
// enqueue方法的synchronized锁对象
private Lock lock = new Lock();
// 队列的头元素
private volatile Reference<? extends T> head = null;
// 队列中元素个数
private long queueLength = 0;
NULL是ReferenceQueue的内部子类,enqueue()方法并不执行入队。
创建Reference时,如果不提供queue时,会将queue设置为NULL。
3.2 入队
enqueue()逻辑如下:
引用已入队时,会将Reference.queue属性设置为ENQUEUED。
队列中,各元素通过Reference.next属性关联,形成单向链表,ReferenceQueue.head始终指向尾元素。
3.3 出队
出队有无阻塞的poll()、带阻塞的remove()。两者都会调用reallyPoll()方法:
- 将ReferenceQueue.head指向next节点,原head出队;
- 如果head为null,即队列中无引用,将返回null。
remove()不带超时,在队列为空时将一直阻,直到有引入队列时才被放行。
3.4 FILO特性
可见,ReferenceQueue不是先进先出的,而是先进后出的单向链表。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef1 = new WeakReference<>(new Object(), queue);
System.out.println("weakRef1: " + weakRef1);
// 手动触发gc
System.gc();
TimeUnit.SECONDS.sleep(5);
WeakReference<Object> weakRef2 = new WeakReference<>(new Object(), queue);
System.out.println("weakRef2: " + weakRef2);
System.gc();
Reference<?> poll;
while ((poll = queue.remove(2000)) != null) {
System.out.println("dequeue:" + poll);
}
执行上面代码,结果如下:weakRef2后入队,但是会先出队。