JDK 源码阅读 Reference

Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即便内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象。

若是只提供强引用,咱们就很难写出“这个对象不是很重要,若是内存不足GC回收掉也是能够的”这种语义的代码。Java在1.2版本中完善了引用体系,提供了4中引用类型:强引用,软引用,弱引用,虚引用。使用这些引用类型,咱们不但能够控制垃圾回收器对对象的回收策略,同时还能在对象被回收后获得通知,进行相应的后续操做。java

引用与可达性分类算法

Java目前有4中引用类型:sql

  1. 强引用(Strong Reference):普通的的引用类型,new一个对象默认获得的引用就是强引用,只要对象存在强引用,就不会被GC。
  2. 软引用(Soft Reference):相对较弱的引用,垃圾回收器会在内存不足时回收弱引用指向的对象。JVM会在抛出OOME前清理全部弱引用指向的对象,若是清理完仍是内存不足,才会抛出OOME。因此软引用通常用于实现内存敏感缓存。
  3. 弱引用(Weak Reference):更弱的引用类型,垃圾回收器在GC时会回收此对象,也能够用于实现缓存,好比JDK提供的WeakHashMap。
  4. 虚引用(Phantom Reference):一种特殊的引用类型,不能经过虚引用获取到关联对象,只是用于获取对象被回收的通知。

相较于传统的引用计数算法,Java使用可达性分析来判断一个对象是否存活。其基本思路是从GC Root开始向下搜索,若是对象与GC Root之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。Java有5中类型的可达性:缓存

  1. 强可达(Strongly Reachable):若是线程能经过强引用访问到对象,那么这个对象就是强可达的。
  2. 软可达(Soft Reachable):若是一个对象不是强可达的,可是能够经过软引用访问到,那么这个对象就是软可达的
  3. 弱可达(Weak Reachable):若是一个对象不是强可达或者软可达的,可是能够经过弱引用访问到,那么这个对象就是弱可达的。
  4. 虚可达(Phantom Reachable):若是一个对象不是强可达,软可达或者弱可达,而且这个对象已经finalize过了,而且有虚引用指向该对象,那么这个对象就是虚可达的。
  5. 不可达(Unreachable):若是对象不能经过上述的几种方式访问到,则对象是不可达的,能够被回收。

对象的引用类型与可达性听着有点乱,好像是一回事,咱们这里实例分析一下:数据结构

JDK 源码阅读 Reference

 

上面这个例子中,A~D,每一个对象只存在一个引用,分别是:A-强引用,B-软引用,C-弱引用,D-虚引用,因此他们的可达性为:A-强可达,B-软可达,C-弱可达,D-虚可达。由于E没有存在和GC Root的引用链,因此它是不可达。多线程

在看一个复杂的例子:架构

JDK 源码阅读 Reference

 

  • A依然只有一个强引用,因此A是强可达
  • B存在两个引用,强引用和软引用,可是B能够经过强引用访问到,因此B是强可达
  • C只能经过弱引用访问到,因此是弱可达
  • D存在弱引用和虚引用,因此是弱可达
  • E虽然存在F的强引用,可是GC Root没法访问到它,因此它依然是不可达。

同时能够看出,对象的可达性是会发生变化的,随着运行时引用对象的引用类型的变化,可达性也会发生变化,能够参考下图:并发

JDK 源码阅读 Reference

 

Reference整体结构分布式

Reference类是全部引用类型的基类,Java提供了具体引用类型的具体实现:函数

JDK 源码阅读 Reference

 

  • SoftReference:软引用,堆内存不足时,垃圾回收器会回收对应引用
  • WeakReference:弱引用,每次垃圾回收都会回收其引用
  • PhantomReference:虚引用,对引用无影响,只用于获取对象被回收的通知
  • FinalReference:Java用于实现finalization的一个内部类

由于默认的引用就是强引用,因此没有强引用的Reference实现类。

Reference的核心

Java的多种引用类型实现,不是经过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变量reference:

 

public abstract class Reference<T> {
 private T referent; // 会被GC特殊对待
 
 // 获取Reference管理的对象
 public T get() {
 return this.referent;
 }
 
 // ...
}

若是JVM没有对这个变量作特殊处理,它依然只是一个普通的强引用,之因此会出现不一样的引用类型,是由于JVM垃圾回收器硬编码识别SoftReference,WeakReference,PhantomReference等这些具体的类,对其reference变量进行特殊对象,才有了不一样的引用类型的效果。

上文提到了Reference及其子类有两大功能:

  1. 实现特定的引用类型
  2. 用户能够对象被回收后获得通知

第一个功能已经解释过了,第二个功能是如何作到的呢?

一种思路是在新建一个Reference实例是,添加一个回调,当java.lang.ref.Reference#referent被回收时,JVM调用该回调,这种思路比较符合通常的通知模型,可是对于引用与垃圾回收这种底层场景来讲,会致使实现复杂,性能不高的问题,好比须要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等等。

因此Reference使用了一种更加原始的方式来作通知,就是把引用对象被回收的Reference添加到一个队列中,用户后续本身去从队列中获取并使用。

理解了设计后对应到代码上就好理解了,Reference有一个queue成员变量,用于存储引用对象被回收的Reference实例:

 

public abstract class Reference<T> {
 // 会被GC特殊对待
 private T referent; 
 // reference被回收后,当前Reference实例会被添加到这个队列中
 volatile ReferenceQueue<? super T> queue;
 
 // 只传入reference的构造函数,意味着用户只须要特殊的引用类型,不关心对象什么时候被GC
 Reference(T referent) {
 this(referent, null);
 }
 
 // 传入referent和ReferenceQueue的构造函数,reference被回收后,会添加到queue中
 Reference(T referent, ReferenceQueue<? super T> queue) {
 this.referent = referent;
 this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
 }
 
 // ...
}

Reference的状态

Reference对象是有状态的。一共有4中状态:

  1. Active:新建立的实例的状态,由垃圾回收器进行处理,若是实例的可达性处于合适的状态,垃圾回收器会切换实例的状态为Pending或者Inactive。若是Reference注册了ReferenceQueue,则会切换为Pending,而且Reference会加入pending-Reference链表中,若是没有注册ReferenceQueue,会切换为Inactive。
  2. Pending:在pending-Reference链表中的Reference的状态,这些Reference等待被加入ReferenceQueue中。
  3. Enqueued:在ReferenceQueue队列中的Reference的状态,若是Reference从队列中移除,会进入Inactive状态
  4. Inactive:Reference的最终状态

Reference对象图以下:

JDK 源码阅读 Reference

 

除了上文提到的ReferenceQueue,这里出现了一个新的数据结构:pending-Reference。这个链表是用来干什么的呢?

上文提到了,reference引用的对象被回收后,该Reference实例会被添加到ReferenceQueue中,可是这个不是垃圾回收器来作的,这个操做仍是有必定逻辑的,若是垃圾回收器还须要执行这个操做,会下降其效率。从另一方面想,Reference实例会被添加到ReferenceQueue中的实效性要求不高,因此也不必在回收时立马加入ReferenceQueue。

因此垃圾回收器作的是一个更轻量级的操做:把Reference添加到pending-Reference链表中。Reference对象中有一个pending成员变量,是静态变量,它就是这个pending-Reference链表的头结点。要组成链表,还须要一个指针,指向下一个节点,这个对应的是java.lang.ref.Reference#discovered这个成员变量。

能够看一下代码:

 

public abstract class Reference<T> {
 // 会被GC特殊对待
 private T referent; 
 // reference被回收后,当前Reference实例会被添加到这个队列中
 volatile ReferenceQueue<? super T> queue; 
 
 // 全局惟一的pending-Reference列表
 private static Reference<Object> pending = null;
 
 // Reference为Active:由垃圾回收器管理的已发现的引用列表(这个不在本文讨论访问内)
 // Reference为Pending:在pending列表中的下一个元素,若是没有为null
 // 其余状态:NULL
 transient private Reference<T> discovered; /* used by VM */
 // ...
}

ReferenceHandler线程

经过上文的讨论,咱们知道一个Reference实例化后状态为Active,其引用的对象被回收后,垃圾回收器将其加入到pending-Reference链表,等待加入ReferenceQueue。这个过程是如何实现的呢?

这个过程不能对垃圾回收器产生影响,因此不能在垃圾回收线程中执行,也就须要一个独立的线程来负责。这个线程就是ReferenceHandler,它定义在Reference类中:

 

// 用于控制垃圾回收器操做与Pending状态的Reference入队操做不冲突执行的全局锁
// 垃圾回收器开始一轮垃圾回收前要获取此锁
// 因此全部占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码
static private class Lock { };
private static Lock lock = new Lock();
 
private static class ReferenceHandler extends Thread {
 
 ReferenceHandler(ThreadGroup g, String name) {
 super(g, name);
 }
 
 public void run() {
 // 这个线程一直执行
 for (;;) {
 Reference<Object> r;
 // 获取锁,避免与垃圾回收器同时操做
 synchronized (lock) {
 // 判断pending-Reference链表是否有数据
 if (pending != null) {
 // 若是有Pending Reference,从列表中取出
 r = pending;
 pending = r.discovered;
 r.discovered = null;
 } else {
 // 若是没有Pending Reference,调用wait等待
 // 
 // wait等待锁,是可能抛出OOME的,
 // 由于可能发生InterruptedException异常,而后就须要实例化这个异常对象,
 // 若是此时内存不足,就可能抛出OOME,因此这里须要捕获OutOfMemoryError,
 // 避免由于OOME而致使ReferenceHandler进程静默退出
 try {
 try {
 lock.wait();
 } catch (OutOfMemoryError x) { }
 } catch (InterruptedException x) { }
 continue;
 }
 }
 
 // 若是Reference是Cleaner,调用其clean方法
 // 这与Cleaner机制有关系,不在此文的讨论访问
 if (r instanceof Cleaner) {
 ((Cleaner)r).clean();
 continue;
 }
 
 // 把Reference添加到关联的ReferenceQueue中
 // 若是Reference构造时没有关联ReferenceQueue,会关联ReferenceQueue.NULL,这里就不会进行入队操做了
 ReferenceQueue<Object> q = r.queue;
 if (q != ReferenceQueue.NULL) q.enqueue(r);
 }
 }
}

ReferenceHandler线程是在Reference的static块中启动的:

 

static {
 // 获取system ThreadGroup
 ThreadGroup tg = Thread.currentThread().getThreadGroup();
 for (ThreadGroup tgn = tg;
 tgn != null;
 tg = tgn, tgn = tg.getParent());
 Thread handler = new ReferenceHandler(tg, "Reference Handler");
 
 // ReferenceHandler线程有最高优先级
 handler.setPriority(Thread.MAX_PRIORITY);
 handler.setDaemon(true);
 handler.start();
}

综上,ReferenceHandler是一个最高优先级的线程,其逻辑是从Pending-Reference链表中取出Reference,添加到其关联的Reference-Queue中。

ReferenceQueue

Reference-Queue也是一个链表:

 

public class ReferenceQueue<T> {
 private volatile Reference<? extends T> head = null;
 // ...
}

 

// ReferenceQueue中的这个锁用于保护链表队列在多线程环境下的正确性
static private class Lock { };
private Lock lock = new Lock();
 
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
 synchronized (lock) {
 // 判断Reference是否须要入队
 ReferenceQueue<?> queue = r.queue;
 if ((queue == NULL) || (queue == ENQUEUED)) {
 return false;
 }
 assert queue == this;
 
 // Reference入队后,其queue变量设置为ENQUEUED
 r.queue = ENQUEUED;
 // Reference的next变量指向ReferenceQueue中下一个元素
 r.next = (head == null) ? r : head;
 head = r;
 queueLength++;
 if (r instanceof FinalReference) {
 sun.misc.VM.addFinalRefCount(1);
 }
 lock.notifyAll();
 return true;
 }
}

经过上面的代码,能够知道java.lang.ref.Reference#next的用途了:

 

public abstract class Reference<T> {
 /* When active: NULL
 * pending: this
 * Enqueued: 指向ReferenceQueue中的下一个元素,若是没有,指向this
 * Inactive: this
 */
 Reference next;
 
 // ...
}

总结

一个使用Reference+ReferenceQueue的完整流程以下:

JDK 源码阅读 Reference

欢迎工做一到五年的Java工程师朋友们加入Java架构开发 : 867748702 群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、 Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper, Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料) 合理利用本身每一分每一秒的时间来学习提高本身, 不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!

相关文章
相关标签/搜索