本文对源码的解析基于JDK13。
1. Java对象的引用
1.1 引用的概念
谈引用之前,我们先看一下指针,在可以操作指针的语言中,指针代表的是内存的地址,通过操作指针我们可以直接操作内存。Java语言不直接操作指针,即不直接操作内存(但是我们可以通过Unsafe
类间接操作内存)。我们所操作的是引用,有了一个对象的引用,我们就可以操作这个对象,而对象分配在内存中,对对象的修改实际上是对内存的读写。所以我们理解的引用本质是别名,实际上引用存储的数值是一块内存的地址。在Java中,如果一块内存存放着另一块内存的地址,我们可以称这块内存为引用。所以Object o = new Object()
中o
本质上是我们刚new出来的对象在内存中地址,无论这个对象是普通对象还是数组对象。
1.2 Reference类解析
首先,万物皆对象。谈Reference
类前,我们先类比Class
类,我们知道Class
类是对类对象的抽象,Class
类的实例表示正在运行的类和接口。Reference
类是对引用对象的抽象,与Class
类不同的是,Reference
类是抽象类,并不能直接实例化。Reference
类是与垃圾回收紧密相关的,该类提供了部分接口以便更好的帮助JVM进行GC。它的子类可被实例化,它的子类的实例一般表示了可以在垃圾回收时可以做额外的操作的引用。此外Reference
类虽然不用Final修饰,但是不能被直接继承,我们只能从jdk自带的默认实现(SoftReference
、WeakReference
等)继承。我们通过对Reference
类源码解读加强对引用和垃圾回收的理解。Reference
在包java.lang.ref
下
1.2.1 构造函数
Reference
类没有默认的无参构造函数,只有两个访问权限为包权限的有参构造函数。
// 构造方法1
Reference(T referent) {
this(referent, null);
}
上述构造方法本质上是下述构造方法中参数 queue=null
的特殊情况。
// 构造方法2
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
从构造方法看Reference
类至少有两个成员变量:referent
和quene
。其中referent
代表该引用对象实际所指向的对象,而queue
代表的是引用队列,队列中的元素是Reference
对象。当使用后一种构造方法构造Reference
对象时,一旦referent
所指向对象的GC可达性状态更改后,即对象在进行可达性分析后发现没有与GC Roots相连接的引用链后,简单来说就是没有任何强引用指向referent
指向的对象,就会将Reference
对象添加进queue
所代表的引用队列。此时用户可以使用referent == null
或者轮询queue
引用队列做相应的清理操作等。
1.2.2 引用队列
上一节提到了引用队列ReferenceQueue
,其节点是Reference
,Reference
类有一个使用volatile
修饰的成员变量next
,该属性指向引用队列中的下一个节点:volatile Reference next;
。而ReferenceQueue
自身持有头节点:private volatile Reference<? extends T> head;
。
在ReferenceQueue
中有一个enqueue
方法,说明了将Reference
节点添加进ReferenceQueue
引用队列的过程。
// ReferenceQueue的enqueue方法,仅被Reference类调用
boolean enqueue(Reference<? extends T> r) {
// 操作队列时需要加锁
synchronized (lock) {
// 构造Reference对象时未传入ReferenceQueue对象,或者Reference对象已经进入队列了
// 则返回入队失败
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
// 确认r的队列是否为当前队列
assert queue == this;
// 此处为头插法添加节点
// 如果头节点为null,则将当前节点的下一节点设置为自己,否则将原先头节设置为该节点的下一节点
r.next = (head == null) ? r : head;
// 将当前节点设置成头节点
head = r;
// 将队列里Reference数量加1
queueLength++;
// 将r的queue设置为ENQUEUED状态
r.queue = ENQUEUED;
// 如果Reference对象为FinalReference类的实例,则将FinalReference数量也加1
if (r instanceof FinalReference) {
VM.addFinalRefCount(1);
}
// 唤醒等待的线程
lock.notifyAll();
return true;
}
}
同样的,在ReferenceQueue
中有一个reallyPoll
方法,说明了将Reference
节点移出ReferenceQueue
引用队列的过程。
// reallyPoll该方法是实际将Reference对象移除出队列的过程,poll/remove底层是调用该方法
// 调用该方法时必须持有锁
private Reference<? extends T> reallyPoll() {
// 获取队列头部的节点
Reference<? extends T> r = head;
if (r != null) {
r.queue = NULL;
// 移除头节点,并将头节点的下一个节点设置为头节点
Reference<? extends T> rn = r.next;
head = (rn == r) ? null : rn;
r.next = r;
queueLength--;
if (r instanceof FinalReference) {
VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
结合enqueue
和reallyPoll
方法可以知道ReferenceQuene
底层是一个由链表组成的队列,但是该队列实际上后进先出(入队列使用头插法,后来的节点是头节点,出队列移除头节点),实际上更像是一个栈。引用队列大体上的作用是在垃圾回收时可以将Reference
放进这个队列中(只有构造Reference对象指定了队列时才入放进去),然后开发人员可以将Reference
取出来后执行自己的操作。
1.2.3 对引用的操作
这里讲的对引用的操作实际讲的就是如何在垃圾回收的时候将Reference
对象放进ReferenceQueue
队列中。
Reference
类有一个静态代码块,当我们new
一个Reference
子类的对象出来时,首先会执行该静态代码块。该静态代码块主要工作是新建了一个最高优先级的ReferenceHandler
守护线程并启动它。
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(); // 启动ReferenceHandler线程
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean waitForReferenceProcessing()
throws InterruptedException
{
return Reference.waitForReferenceProcessing();
}
@Override
public void runFinalization() {
Finalizer.runFinalization();
}
});
}
该静态代码块主要工作是新建了一个最高优先级的ReferenceHandler
守护线程并启动它。
private static class ReferenceHandler extends Thread {
private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
static {
// 预加载并初始化Cleaner类,防止在后面run循环中懒加载或者初始化Cleaner类内存不足出现异常
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, null, name, 0, false);
}
public void run() {
while (true) {
processPendingReferences(); // ReferenceHandler守护线程的工作
}
}
ReferenceHandler
类是Reference
类的静态内部类,它的run方法是一个无限循环执行processPendingReferences
方法将pending-Reference list
中的引用对象加入到ReferenceQueue
队列中。
private static void processPendingReferences() {
// Reference类的native方法,等待JVM在GC时构建pending-Reference list
waitForReferencePendingList();
Reference<Object> pendingList;
// 加锁,防止多个线程对pending-Reference list操作产生竞争
synchronized (processPendingLock) {
// Reference类的native方法,原子方式获取pending-Reference list队列并清空该队列
pendingList = getAndClearReferencePendingList();
processPendingActive = true;
}
while (pendingList != null) {
Reference<Object> ref = pendingList;
pendingList = ref.discovered; // 获取等待队列中的下一个Reference对象
ref.discovered = null;
// Cleaner对象在做清除操作时做额外的处理
if (ref instanceof Cleaner) {
((Cleaner)ref).clean();
synchronized (processPendingLock) {
processPendingLock.notifyAll();
}
} else {
ReferenceQueue<? super Object> q = ref.queue;
// 在GC回收referent所指向的对象后将Reference对象加入到ReferenceQueue队列中
if (q != ReferenceQueue.NULL) q.enqueue(ref);
}
}
synchronized (processPendingLock) {
processPendingActive = false;
processPendingLock.notifyAll();
}
}
在这里我们看到了一个新的队列pending-Reference list
,以及一个新的成员变量discovered
。由pendingList = ref.discovered;
可知pending-Reference list
通过Reference
对象的成员变量discovered
连接——private transient Reference<T> discovered;
。pending-Reference list
并不是由用户生成和维护,它是由JVM生成和维护,每次JVM在进行GC的时候,就会生成和填充pending-Reference list
队列,队列里是由垃圾收集器维护的需要重新访问的访问的Reference
对象,以便确认是否在垃圾回收的时候对用户进行通知——即加入ReferenceQueue
队列中。综上所述,当Reference
对象所指向的对象被回收时,Reference
对象就会进入由JVM维护的pending-Reference list
队列,然后由ReferenceHandler
线程进行处理,当构建Reference
对象传入ReferenceQueue
参数后,该Reference
对象就会进入到ReferenceQueue
队列中,最终起到一个垃圾回收时通知的作用。
Reference
类的API不多,主要有get
、clear
、isEnqueued
、enqueue
、reachabilityFence
方法。
get
方法返回该Reference
对象所实际指向的对象,如果程序或者垃圾收集器清除了该引用对象,则返回null。假设实际指向的对象的强引用已经消失但是还未被垃圾收集器清除,通过该方法返回的对象可以重新变成强引用。
clear
方法清除该Reference
对象所实际指向的对象,但是调用该方法并不会导致该引用对象进入引用队列。该方法仅会被java代码调用,垃圾收集器进行垃圾回收时不会调用该方法而可以直接清除引用对象。
isEnqueued
方法告诉用户该Reference
对象是否已经进入引用队列(仅当创建Reference
对象时传入引用队列时起作用)。
enqueue
方法主动清除该Reference
对象所实际指向的对象并将该引用对象加入引用队列(仅当创建Reference
对象时传入引用队列时起作用)。该方法仅会被java代码调用,垃圾收集器进行垃圾回收时不会调用该方法而可以直接清除引用对象并将该引用对象如队列。
reachabilityFence
是可达性栅栏,是JDK9新增的方法。调用该方法可以使得一个对象在没有强引用后仍然存活至调用该代码的位置,至少在调用该方法之后,垃圾回收才能回收引用的对象,调用该方法不会导致垃圾回收。常用于Excutors、HTTP2客户端等经常会异步调用的情况。
1.2.4 引用的状态
官方给引用定义了两种类型的状态,但是引用的状态没有在Reference
类中专门定义,而是通过referent
和queue
等成员变量共同决定。这两种类型的状态Reference
对象可同时拥有。
第一种类型状态有三种,主要和referenct
成员变量的值相关,分别是Active
,Pending
和Inactive
。第二种类型状态有四种,主要和queue
成员变量的值相关,分别是Registered
,Enqueued
,Dequeued
和Unregistered
。
由Reference
类的构造方法可知,创建Reference
对象时传入引用队列则引用对象会有Registered
,Enqueued
,Dequeued
三种状态,而未传入引用队列时则只有Unregistered
一种状态。引用对象正常存活时则起始状态必然为Active
,被垃圾收集器垃圾回收后状态必然为Inactive
。则引用对象起始状态有Active/Registered
和Active/Unregistered
两种,而终结状态有Inactive/Dequeued
和Inactive/Unregistered
两种。故引用状态变迁有Active/Unregistered
->Inactive/Unregistered
和Active/Registered
->Inactive/Dequeued
两种路线。
第一条:Active/Unregistered
->Inactive/Unregistered
第二条:Active/Registered
->Inactive/Registered
其中Active/Registered
->Inactive/Registered
组合的9种状态中不可能存在Active/Enqeued
和Active/Dequeued
两种状态,因为enqueue
方法会做清除操作,此时不可能是Active
状态。
public boolean enqueue() {
this.referent = null; // 此处与clear方法相同
return this.queue.enqueue(this);
}
此外已在上文介绍过GC
、clear
、enqueue
、ReferenceHandler
等操作,剩余的poll
方法和remove
方法是ReferenceQueue
类的方法,主要做的操作就是清除引用队列的引用对象,所以此时的引用的状态变化均为Enqeued
->Dequeued
。
2. 引用的类型
2.1 强引用
对于强引用的一般理解就是new
出来的引用就是强引用,Object o = new Object()
中的o
就是强引用,当强引用关系存在时,垃圾回收期就不会回收被强引用引用的对象,当JVM内存不足即将抛出OOM
时,GC
回收也不会回收强引用所关联的对象内存,会直接抛出OOM
。当然上文中提到的reachabilityFence
可达性栅栏也能达到强引用关系。在Java
类库中没有一个专门的类代表强引用。
2.2 软应用
软引用用来描述那些有用但是非必需的对象。垃圾收集器会根据内存需求酌情回收仅有软引用所关联的对象:当JVM
内存不足即将抛出OOM
时,触发GC
回收软引用所关联的对象内存。而JVM
内存充足时,即使进行GC
也不会进行回收。在Java
类库中SoftReference
类代表软引用。在SoftReference
类中指出软引用常用于实现对内存敏感的缓存,但其实缓存上的实现我们常用redis
。
2.3 弱引用
2.3.1 弱引用的概念和作用
弱引用同样用来描述那些有用但是非必需的对象,通常认为,弱引用的强度要比软引用低,这个强度表现在垃圾回收时:仅有弱引用所关联的对象内存在遭遇GC
时就会进行回收,无论当前JVM
内存是否足够。在Java
类库中WeakReference
类代表弱引用。
2.3.2 弱引用的使用场景
在WeakReference
类中指出弱引用常用于实现规范化映射,在JDK
中常用于自定义哈希表。具体实现包括ThreadLocal
中的ThreadLocalMap
实现与WeakHashMap
实现。
ThreadLocalMap
内部Entry
的实现,内部重写了WeakReference
类的构造方法public WeakReference(T referent) { super(referent); }
,ReferenceQueue
为null
,内部通过get()
方法判断引用是否失效:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用父类的构造方法,不含ReferenceQueue参数
value = v;
}
}
WeakHashMap
内部Entry
的实现,内部重写了WeakReference
类的构造方法public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); }
,ReferenceQueue
为自己声明并传入的队列,具体为queue``private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
,内部通过queue.poll()
方法判断引用是否失效:
private static class Entry<K,V> extends WeakReference<Object>
implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue); // 调用父类的构造方法,包含ReferenceQueue参数
this.value = value;
this.hash = hash;
this.next = next;
}
// 以下省略了Entry内部的equals等方法。
}
2.4 虚引用
2.4.1 虚引用的概念和作用
虚引用与软引用、弱引用不同,对一个对象关联虚引用,并不会影响对该对象的GC
,仅仅是为了虚引用关联的对象在GC
时能收到一个通知,使用者在接收到通知后可以进行事后的清理操作等。
在Java
类库中PhantomReference
类代表虚引用,与SoftReference
、WeakReference
类均有两个构造方法不同,PhantomReference
类只有一个构造方法:
public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); }
,即创建虚引用对象时必须传入一个引用队列对象,当referent
被回收时,就将虚引用关联的Reference
对象加入到引用队列q
中,当使用者在引用队列中检查到Reference
对象入队,就可以做其他额外的操作,这就是GC
时能收到一个通知。
2.4.2 虚引用的使用场景
虚引用可以用来管理直接内存,即堆外分配的内存。因为垃圾回收器仅可以回收JVM
自身所使用的内存,对于直接内存的回收需要使用者自己进行回收,而何时进行回收就需要使用到虚引用的通知作用了。
PhantomReference
有一个子类Cleaner
。nio
中的DirectByteBuffer
类成员变量cleaner
就是在回收直接内存前起到回收通知的作用,后续调用Unsafe
类的freeMemory
方法回收内存进行清理操作。
2.5 FinalReference
除了SoftReference
、WeakReference
、PhantomReference
类外,JDK
中还有一个Reference
类的子类实现FinalReference
。该类是默认的包访问权限,有一个扩展实现Finalizer
,该类是包访问权限且使用final修饰,所以该类仅仅为JVM
使用。当一个类重写了Object
类的finalize
方法,该类的对象自创建后就会标记成Finalizer
对象以用于JVM
进行垃圾回收。
2. 6总结
对于引用类型,主要记住强软弱虚四种类型,强引用存在不会进行GC
回收,软引用存在只在即将发生OOM
时进行GC
回收,弱引用存在时在下一次GC
就会被回收,虚引用不会影响GC
回收,只是在回收时起到通知的作用。
参考:
https://www.jianshu.com/p/30157b64a51a
https://www.jianshu.com/p/9bb3df3556e2
https://blog.youkuaiyun.com/gdutxiaoxu/article/details/80738581
https://www.cnblogs.com/yaowen/p/10841683.html
https://blog.youkuaiyun.com/zqz_zqz/article/details/79474314
http://lovestblog.cn/blog/2015/07/09/final-reference/