别睡!打起精神来学习!
📖什么是Reference
Reference本身是一个抽象类,它的实现类有SoftReference
,WeakReference
,PhantomReference
,FinalReference
。GC回收器会与该类中的变量做直接交互。
当垃圾收集器检测到referent
对象可达性为不可达时,Reference的实例状态将从Active
变更为Pending
,当实例被ReferenceHandler
放入ReferenceQueue
中时,它的状态从Pending
转换为Enqueued
,当ReferenceQueue
消费完成后,则变换为最终状态Inactive
,此时实例会被释放掉。
Reference用于管理对象自身的四种状态:
- Active 新创建的Reference的实例状态
- Pending 实例即将被
ReferenceHandler
线程加入ReferenceQueue
队列的状态 - Enqueued 实例在
ReferenceQueue
队列中,处于消费中或等待中的状态 - Inactive 最终状态
♾️垃圾回收器
🚩可达性分析算法
JVM判断一个对象是否存活时,默认使用了可达性分析算法
。通过GC Root
对象向下搜索,搜索到的对象都是可达对象,而剩余对象全都是不可达对象。如下图,红色为不可达对象,蓝色为可达对象。
我们可以看到只有在GC Root这条链上并被这条引用链引用的对象才可以被认定为可达对象。
GC Root对象有哪些呢?
- 本地方法栈JNI引用的对象,也就是Native引用的对象
- 虚拟机栈中引用的对象,也就是栈帧中的本地变量(说白了就是执行方法中new的对象)
- 方法区里常量引用的对象
- 方法区中静态属性引用的对象
🌲Reference源码解读
首先来看一下Reference
的核心成员变量
private T referent;
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
transient private Reference<T> discovered;
private static Reference<Object> pending = null;
private static Lock lock = new Lock();
- referent 表示该对象引用的对象实例,在创建引用时就需要指定该实例
- queue 对象触发回收时需要通知的队列,当触发回收时,会将整个referent实例放入队列中,消费者可以指定队列进行消费
- next 指向引用队列中的下一个Reference实例
- discovered 维护引用列表中的下一个元素,当为最后一个元素时,该值为null
- pending 等待通知的引用链表,垃圾回收器会将即将回收的对象实例加入到链表中
- lock 锁对象,用于垃圾收集器的同步操作,防止GC和tryHandlePending同时操作pending
注意Refrence
的静态代码块
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
// 遍历主线程组中全部线程
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
// 为该线程设置最大的优先级
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 初始化尝试通知一次
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
可以看到此处主要做的动作就是遍历主线程组中所有线程,并为每个线程创建ReferenceHandler
线程对象并设为了守护线程,我们看看它的run方法
public void run() {
while (true) {
tryHandlePending(true);
}
}
此处循环调用了tryHandlePending
方法,其核心逻辑是扫描pending
链表是否有数据,如果有,则取出链表头并放入通知队列。
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 判断是不是Cleaner对象,如果是则赋值,后面做特殊处理
c = r instanceof Cleaner ? (Cleaner) r : null;
// 将当前pending从链表中解除,并将其下一个设为链头
pending = r.discovered;
r.discovered = null;
} else {
// 当pending为null时则等待gc通知
if (waitForNotify) {
lock.wait();
}
// 返回什么都一样
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// 当内存溢出时,让出当前cpu,重新等待分配并重试
Thread.yield();
return true;
} catch (InterruptedException x) {
// InterruptedException异常直接重试
return true;
}
// 做特殊处理
if (c != null) {
c.clean();
return true;
}
// 入通知队列
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
我们对以上整段流程做个总结,有专门的守护线程扫描是否有新的被回收的Reference对象被放入队列了,如果有则将其放入队列中,等待订阅者消费,流程如下图。
🌲ReferenceQueue队列源码解读
Reference通知队列用的是ReferenceQueue,并不是像LinkedBlockingQueue这样的队列,不用LinkedBlockingQueue的原因在于ReferenceQueue的应用场景仅作为回收后通知,并不存在特别高的并发,因此ReferenceQueue只使用了一把锁。其次ReferenceQueue基于Reference对象实现,天然支持next引用,不像LinkedBlockingQueue实现逻辑那么复杂,还需要维护Node对象。
接下来我们从ReferenceQueue的核心成员变量看起
private Lock lock = new Lock();
private volatile Reference<? extends T> head = null;
private long queueLength = 0;
- lock 全局锁对象
- head 当前链表的头对象
- queueLength 当前队列的长度
我们看一下放入队列的方法enqueue
,该方法本质上是将Reference自身放入到自己成员变量的queue队列中
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
// 操作加锁
synchronized (lock) {
// 校验通知队列不能为空
ReferenceQueue<?> queue = r.queue;
// 如果是Null 或者 已被通知就跳出
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// 标记已通知
r.queue = ENQUEUED;
// 设置队列中下一个Reference对象
r.next = (head == null) ? r : head;
// 设置队列头对象
head = r;
// 记录队列数量
queueLength++;
// 如果是FinalReference实例 则它的计数器也+1
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
remove
方法,提供了等待超时时长
public Reference<? extends T> remove(long timeout)
throws IllegalArgumentException, InterruptedException
{
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value");
}
// 使用全局锁
synchronized (lock) {
// 从队列中取值
Reference<? extends T> r = reallyPoll();
if (r != null) return r;
// 值不存在则循环等待获取
long start = (timeout == 0) ? 0 : System.nanoTime();
for (;;) {
lock.wait(timeout);
r = reallyPoll();
if (r != null) return r;
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null;
start = end;
}
}
}
}
reallyPoll
核心取值方法
private Reference<? extends T> reallyPoll() { /* Must hold lock */
// 得到队列头部
Reference<? extends T> r = head;
if (r != null) {
// 将next引用置为链表头部
@SuppressWarnings("unchecked")
Reference<? extends T> rn = r.next;
head = (rn == r) ? null : rn;
// 标记消费
r.queue = NULL;
// 移除next引用
r.next = r;
// 队列计数器-1
queueLength--;
// 如果是FinalReference 则计数器-1
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
⭐️推荐阅读
通过本章我们了解了Reference
的原理和ReferenceQueue
的实现原理,以及为什么要使用ReferenceQueue作为队列。如果你还不知道Reference
如何使用可阅读这篇文章:JAVA中的引用类型,强引用软引用弱引用虚拟引用