JDK源码阅读-Reference
本文转载自JDK源码阅读-Reference
导语
Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象。
如果只提供强引用,我们就很难写出“这个对象不是很重要,如果内存不足GC回收掉也是可以的”这种语义的代码。Java在1.2版本中完善了引用体系,提供了4中引用类型:强引用,软引用,弱引用,虚引用。使用这些引用类型,我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作。
引用与可达性分类
Java目前有4中引用类型:
- 强引用(Strong Reference):普通的的引用类型,new一个对象默认得到的引用就是强引用,只要对象存在强引用,就不会被GC。
- 软引用(Soft Reference):相对较弱的引用,垃圾回收器会在内存不足时回收弱引用指向的对象。JVM会在抛出OOME前清理所有弱引用指向的对象,如果清理完还是内存不足,才会抛出OOME。所以软引用一般用于实现内存敏感缓存。
- 弱引用(Weak Reference):更弱的引用类型,垃圾回收器在GC时会回收此对象,也可以用于实现缓存,比如JDK提供的WeakHashMap。
- 虚引用(Phantom Reference):一种特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。
相较于传统的引用计数算法,Java使用可达性分析来判断一个对象是否存活。其基本思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。Java有5中类型的可达性:
- 强可达(Strongly Reachable):如果线程能通过强引用访问到对象,那么这个对象就是强可达的。
- 软可达(Soft Reachable):如果一个对象不是强可达的,但是可以通过软引用访问到,那么这个对象就是软可达的
- 弱可达(Weak Reachable):如果一个对象不是强可达或者软可达的,但是可以通过弱引用访问到,那么这个对象就是弱可达的。
- 虚可达(Phantom Reachable):如果一个对象不是强可达,软可达或者弱可达,并且这个对象已经finalize过了,并且有虚引用指向该对象,那么这个对象就是虚可达的。
- 不可达(Unreachable):如果对象不能通过上述的几种方式访问到,则对象是不可达的,可以被回收。
对象的引用类型与可达性听着有点乱,好像是一回事,我们这里实例分析一下:

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

- A依然只有一个强引用,所以A是强可达
- B存在两个引用,强引用和软引用,但是B可以通过强引用访问到,所以B是强可达
- C只能通过弱引用访问到,所以是弱可达
- D存在弱引用和虚引用,所以是弱可达
- E虽然存在F的强引用,但是GC Root无法访问到它,所以它依然是不可达。
同时可以看出,对象的可达性是会发生变化的,随着运行时引用对象的引用类型的变化,可达性也会发生变化,可以参考下图:

Reference总体结构
Reference类是所有引用类型的基类,Java提供了具体引用类型的具体实现:

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及其子类有两大功能:
- 实现特定的引用类型
- 用户可以对象被回收后得到通知
第一个功能已经解释过了,第二个功能是如何做到的呢?
一种思路是在新建一个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中状态:
Active:新创建的实例的状态,由垃圾回收器进行处理,如果实例的可达性处于合适的状态,垃圾回收器会切换实例的状态为Pending或者Inactive。如果Reference注册了ReferenceQueue,则会切换为Pending,并且Reference会加入pending-Reference链表中,如果没有注册ReferenceQueue,会切换为Inactive。Pending:在pending-Reference链表中的Reference的状态,这些Reference等待被加入ReferenceQueue中。Enqueued:在ReferenceQueue队列中的Reference的状态,如果Reference从队列中移除,会进入Inactive状态Inactive:Reference的最终状态
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的完整流程如下:

参考资料
- Java Reference详解 - robin-yao的个人页面 - 开源中国
- Internals of Java Reference Object
- java.lang.ref (Java Platform SE 7 )
- Java Reference Objects
- Java核心技术36讲 第4讲
JDK源码阅读-Reference的更多相关文章
- JDK源码阅读-DirectByteBuffer
本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...
- JDK源码阅读(三):ArraryList源码解析
今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...
- JDK源码阅读(一):Object源码分析
最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...
- 利用IDEA搭建JDK源码阅读环境
利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...
- JDK源码阅读-FileOutputStream
本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...
- JDK源码阅读-FileInputStream
本文转载自JDK源码阅读-FileInputStream 导语 FileIntputStream用于打开一个文件并获取输入流. 打开文件 我们来看看FileIntputStream打开文件时,做了什么 ...
- JDK源码阅读-ByteBuffer
本文转载自JDK源码阅读-ByteBuffer 导语 Buffer是Java NIO中对于缓冲区的封装.在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接.但是在J ...
- JDK源码阅读-RandomAccessFile
本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...
- JDK源码阅读-FileDescriptor
本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...
随机推荐
- Java——I/O,字节流与字符流,BufferedOutputStream,InputStream等(附相关练习代码)
I/O: I/O是什么? 在程序中,所有的数据都是以流的形式进行传输或者保存. 程序需要数据的时候,就要使用输入流读取数据. 程序需要保存数据的时候,就要使用输出流来完成. 程序的输入以及输出都是以流 ...
- Java泛型中的通配符T,E,K,V
Java泛型中的通配符T,E,K,V 1.泛型的好处 2.泛型中的通配符 2.1 T,E,K,V,? 2.2 ?无界通配符 2.3 上界通配符 < ? extends E> 2.4 下界通 ...
- Flink-v1.12官方网站翻译-P003-Real Time Reporting with the Table API
利用表格API进行实时报告 Apache Flink提供的Table API是一个统一的.关系型的API,用于批处理和流处理,即在无边界的.实时的流或有边界的.批处理的数据集上以相同的语义执行查询,并 ...
- 2019牛客暑期多校训练营(第十场)F.Popping Balloons(线段树)
题意:现在给你n个点 现在让你横着划三条线间距为r 然后竖着划三条线间距同样为r 现在让你求经过最多的点数 思路:我们首先建一棵关于y区间的线段树 然后枚举x轴 每次更新重叠的点 然后再更新回去 找一 ...
- Educational Codeforces Round 39
Educational Codeforces Round 39 D. Timetable 令\(dp[i][j]\)表示前\(i\)天逃课了\(j\)节课的情况下,在学校的最少时间 转移就是枚举第\ ...
- Codeforces Round #627 (Div. 3) C - Frog Jumps(逻辑)
题意: 有一个每个单元标明移动方向的长为n的序列,每次移动不能超过距离k,问能够从0移动到n+1的k的最小值. 思路: k=最长连续L序列长度+1. #include <bits/stdc++. ...
- Codeforces Round #296 (Div. 2B. Error Correct System
Ford Prefect got a job as a web developer for a small company that makes towels. His current work ta ...
- hdu4770 Lights Against Dudely
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission ...
- Codeforces Round #653 (Div. 3) D. Zero Remainder Array (数学,模拟)
题意:有一组数,刚开始时\(x=0\),每次可以让\(x\)++或让某一个元素+=\(x\)后\(x\)++,每个元素只能加一次\(x\),问最少操作多少次使得所有元素能被\(k\)整除. 题解:每个 ...
- Linux下的C语言编程
一.在Linux下的C语言编程所需工具(下面所列举的都是我编程所用的工具) 1.文本编辑器 Vim 2.编译链接工具 gcc 3.调试器 gdb 4.项目管理工具 make和makefile 二.VI ...