引用简介及分类
1.简介
在JDK1.2以前,java中的引用的定义还是比较传统的:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。引用指向对象的内存地址,对象只有被引用和没被引用两种状态。
实际上,我们更希望存在这样的一类对象:当内存空间还足够的时候,这些对象能够保留在内存空间中;如果当内存空间在进行了垃圾收集之后还是非常紧张,则可以抛弃这些对象。基于这种特性,可以满足很多系统的缓存功能的使用场景。
所以,java对引用的概念进行了扩充。
2.分类
java中将引用分为四种类型:强、软、弱、虚。
-
强引用(StrongReference ) -
软引用(SoftReference ) -
弱引用:(WeakReference ) -
虚引用:(PhantomReference)
通常我们开发中创建的对象都是强引用,不用显式的指明。
Reference
是上述几种引用的父类,SoftReference
,WeakReference
,PhantomReference
都继承了Reference
。因为强引用不需要指定,所以java中没有StrongReference
这个类。
引用类型跟JVM的垃圾回收行为有关。
几种引用的强度依次递减。
3.使用场景
-
强引用(StrongReference ):如果一个对象具备强引用,垃圾回收器绝不会回收它。当内存空间不足,JVM宁愿抛出 OutOfMemoryError
错误,使程序异常终止,也不会出现回收具有强引用的对象来解决内存不足的情况。 -
软引用(SoftReference ):对于软引用关联着的对象,在JVM应用即将发生内存溢出异常之前,将会把这些软引用关联的对象列进去回收对象范围之中进行第二次回收。如果这次回收之后还是没有足够的内存,才会抛出内存溢出异常。 -
弱引用:(WeakReference ):被弱引用关联的对象只能生存到下一次垃圾收集发生之前,简言之就是:一旦发生GC必定回收被弱引用关联的对象,不管当前的内存是否足够。也就是弱引用只能活到下次GC之时。 -
虚引用:(PhantomReference):一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例( PhantomReference
覆盖了Reference#get()
并且总是返回null)。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个 系统通知。
源码分析
1. Reference及ReferenceQueue
先看Reference
的成员和构造方法:
public abstract class Reference<T> {
private T referent;
volatile ReferenceQueue<? super T> queue;
volatile Reference next;
private transient Reference<T> discovered;
Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } } 复制代码
-
referent
代表这个引用关联的对象 -
queue
引用队列。如果引用关联的对象即将被垃圾回收器回收,那个该对象会被添加到这个队列中。在关联对象时可以不指定队列,那么queue
的值就是ReferenceQueue.NULL
,后续在入队过程中,检测到当前引用拥有的时这个队列,会直接返回false。 -
next
引用链表中的下一个元素。虽然引用有引用队列,但是引用是通过这个来形成单向链表的,并不依赖ReferenceQueue
,ReferenceQueue
中只保存了这个链表的head
节点。 -
discovered
基于状态表示不同链表中的下一个待处理的对象,主要是pending-reference列表的下一个元素,通过JVM直接调用赋值。
对于这些引用的处理,在java后台会有一个专门的守护线程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 { // 静态块中,保证类被加载 ensureClassInitialized(InterruptedException.class); ensureClassInitialized(Cleaner.class); } ReferenceHandler(ThreadGroup g, String name) { super(g, name); } // 重写run()方法 public void run() { while (true) { tryHandlePending(true); } } } 复制代码
ReferenceHandler
继承Thread
,重写了run()
方法,里面是个死循环,执行tryHandlePending()
,处理下一个pending元素。
静态块:
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守护线程,并启动。
再回过头分析死循环中的**tryHandlePending()**方法:
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) { // 如果pending!=null,则后续进行处理 r = pending; // 关于Cleaner,我也不太懂,有待研究,知道是和finalize垃圾回收有关 c = r instanceof Cleaner ? (Cleaner) r : null; // 当前引用的下一个元素即将成为新的pending pending = r.discovered; // 置空当前pending的下一个引用(不需要维护这个关系了,上一步已经用完了) r.discovered = null; } else { // 如果pending==null,线程挂起,等待被唤醒 if (waitForNotify) { lock.wait(); } // 从等待状态中恢复后return,继续执行本方法(因为是死循环) return waitForNotify; } } } // 省略部分代码 ............... // Fast path for cleaners if (c != null) { c.clean(); return true; } // 这里处理 pending!=null 的情况,这里pending引用的类型不确定,但是都是将引用的关联对象加入到关联的队列中 ReferenceQueue<? super Object> q = r.queue; // q != ReferenceQueue.NULL 判断当前引用是否关联了引用队列,前面讲过reference的构造方法中queue可以为null,也就是不关联引用队列 // 如果关联了引用队列,才会有入队操作,否则不进行处理。 if (q != ReferenceQueue.NULL) q.enqueue(r); return true; } 复制代码
这个方法就是从pending链表中取出元素然后加入到对象的queue队列中.。
注意到变量pending和方法tryHandlePending都是静态的,他们是属于类的,并不是属于某个对象的。
pending代表当前要处理的引用,这个引用可能是SoftReference ,也有可能是WeakReference ,这都没有关系,不同的引用在这里都是执行相同的逻辑:将引用的关联对象加入到关联的队列中去。
分析**enqueue(r)**方法:
boolean enqueue(Reference<? extends T> r) {
synchronized (lock) {
ReferenceQueue<?> queue = r.queue;
// 如果queue是NULL,则是没有关联引用队列,再一次进行了判断
// 如果queue是ENQUEUED,则表明该
if ((queue == NULL) || (queue == ENQUEUED)) { return false; } assert queue == this; // 入队之后引用关联的队列就变成了固定的ENQUEUED,呼应前一步的判断 r.queue = ENQUEUED; // 将当前引用添加到head元素前面(原来的head成了当前引用的next),都是链表基本操作 r.next = (head == null) ? r : head; head = r; queueLength++; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(1); } lock.notifyAll(); return true; } } 复制代码
入队主要的过程:将即将加入的引用添加到链表的头部,队列长度+1.
不是说引用队列吗?怎么将引用加入到了链表中呢?
我们注意到,Reference中有个next成员变量,如果引用关联的对象被回收,那么这些引用会通过刚才的enqueue()方法和next变量形成一个单向链表。而ReferenceQueue名为队列,其实并不保存这些引用,仅仅保存了这个链表的头部元素,每次有新的引用入队,随之改变队列的head即可。
分析出队:
public Reference<? extends T> poll() {
// 头部元素为空,链表中不存在元素,返回null
if (head == null)
return null;
synchronized (lock) {
// 调用下面的方法 return reallyPoll(); } } private Reference<? extends T> reallyPoll() { /* Must hold lock */ Reference<? extends T> r = head; if (r != null) { Reference<? extends T> rn = r.next; head = (rn == r) ? null : rn; r.queue = NULL; r.next = r; queueLength--; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(-1); } return r; } return null; } 复制代码
因为已知head节点,所以取出元素很方便,根据链接关系就可以顺利找到下一个元素并将其置为新的head。
JVM在GC时如果当前对象只被Reference对象引用,JVM会根据Reference具体类型与堆内存的使用情况决定是否把对应的Reference对象加入到一个由Reference构成的pending链表上,如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程收到通知后会调用Cleaner#clean或ReferenceQueue#enqueue方法进行处理。
几个QueueList:
每个Reference都可以看成是一个节点,多个Reference通过next,discovered和pending这三个属性进行关联。
通过next属性,可以构建ReferenceQueue。就是关联对象被回收的引用形成的链表。
通过pending属性,可以构建Pending List。就是即将ReferenceQueue的引用。
通过discovered属性,可以构建Discovered List。
注意到:next与discovered是非静态的,pending是静态变量。
个人愚见:pending是静态变量,PendingList的元素是全局的,包括SoftReference 、WeakReference 等各种Reference对象,都是通过ReferenceHandler后台线程来加入各自关联队列的。而next和discovered是属于某个对象的,形成的链表的元素都是关联到同一个ReferenceQueue的Reference对象。当多个Reference对象被创建后,对象状态为active,此时形成DiscovedList,discovered指的是在DiscovedList中的下一个元素;当某个Reference对象的状态不是active,而是pending时,该对象从DiscovedList中断开,加入pendingList,discovered指的是在pendingList中的下一个元素。
小结:
引用类型 | 被垃圾收集器回收的时机 | 主要用途 | 生命周期 |
---|---|---|---|
强引用 | 直到OOM也不会被回收 | 普遍对象的状态 | 从创建到JVM实例终止运行 |
软引用 | 内存不足时会被回收 | 有用但非必须的对象缓存 | 从创建到垃圾回收并且内存不足时 |
弱引用 | 垃圾回收时,只能活到下一次GC | 非必须的对象缓存 | 从创建到下一次垃圾回收开始 |
虚引用 | 不影响对象的垃圾回收 | 关联的对象被垃圾收集器回收时候得到一个系统通知 | - |
加入队列(链表)的的元素时什么?不是关联对象,而是引用Reference对象!!!
Reference对象什么时候会添加到队列?关联对象被回收时!!!
2.Finalizer及FinalReference
Reference的子类中有一个特殊的类型FinalReference,Finalizer是FinalReference的子类,和Object#finalize()
有关。
2.1 finalize
在所有引用类型的父类Object中含有一个finalize方法,这个跟垃圾回收有关。我们可以重写这个方法,在方法内进行最后的资源释放等操作。
finalize()如何影响垃圾回收呢?
在GC算法中,有标记-清楚、标记-整理算法,这里不深入讨论这两个算法的具体过程和区别,只讨论他们的标记过程,标记分为两个阶段:
-
第一次标记:如果对象被判定为不可达对象,也就是从GC ROOTS开始搜索,没有一条引用链可达,这个对象就会被第一次标记,等待被回收。 -
第二次标记:将第一次标记的对象再进行判定,分析这个对象是否有必要执行**finalize()**方法,如果有必要执行,就从待回收的集合中剔除,放置在一个叫 F-Queue
的队列之中,并且稍后由一个优先级低的Finalizer线程去取该队列的元素,"尝试执行"元素的finalize()
方法。 是否有必要执行,有两点:-
对象没有覆盖继承自Object类的 finalize()
方法,如果没有重写这个方法,没必要执行。 -
对象的 finalize()
方法已经被JVM调用过,已经调用过了就不会重复执行。
-
2.2 Finalizer
Finalizer继承自FinalReference和Reference,Reference的机制它都具备。在分析源码之前,先用两张图来简述被回收的对象执行finalize()的过程:
源码分析:
先看一下Finalzer的成员变量:
final class Finalizer extends FinalReference<Object> {
// Finalizer关联的ReferenceQueue,其实Finalizer是一个特殊的Reference实现
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 等待finalization的所有Finalizer实例链表的头节点,这里称此链表为unfinalized链表
private static Finalizer unfinalized = null;
private static final Object lock = new Object(); // 中间变量,分别记录unfinalized链表中当前执行元素的下一个节点和前一个节点 private Finalizer next = null, prev = null; } 复制代码
当垃圾对象有必要执行finalize方法时,虚拟机会注册一个Finalizer,并将垃圾对象关联到此Finalizer的referent
上,注册方法:
/* Invoked by VM */
static void register(Object finalizee) { // 垃圾对象
// 调用构造方法
new Finalizer(finalizee);
}
private Finalizer(Object finalizee) { super(finalizee, queue); add(); } 复制代码
Object finalizee
就是待回收的垃圾对象,register
是由虚拟机调用的,使用Finalizer的构造方法,将垃圾对象和成员变量队列queue关联上,Finalizer对象稍后会被加入到这个队列中。
然后再看一下FinalizerThread
线程:
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() { // in case of recursive call to run() if (running) return; // Finalizer thread starts before System.initializeSystemClass // is called. Wait until JavaLangAccess is available while (!VM.isBooted()) { // delay until VM completes initialization try { VM.awaitBooted(); } catch (InterruptedException x) { // ignore and continue } } // 主要的调用过程 final JavaLangAccess jla = SharedSecrets.getJavaLangAccess(); running = true; for (;;) { try { Finalizer f = (Finalizer)queue.remove(); f.runFinalizer(jla); } catch (InterruptedException x) { // ignore and continue } } } } 复制代码
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2); finalizer.setDaemon(true); finalizer.start(); } 复制代码
FinalizerThread重写了Thread的run()方法,并且是在静态块中初始化并启动的。
前面的代码可以不看,直接分析下面的死循环:
Finalizer f = (Finalizer)queue.remove()
作用是从关联的队列中取出Finalizer元素,然后执行Finalizer的runFinalizer
方法。
runFinalizer()方法:
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try { //先获取到垃圾对象 Object finalizee = this.get(); if (finalizee != null && !(finalizee instanceof java.lang.Enum)) { // 真正调用对象的finalize()方法了 jla.invokeFinalize(finalizee); /* Clear stack slot containing this variable, to decrease the chances of false retention with a conservative GC */ finalizee = null; } } catch (Throwable x) { } super.clear(); } 复制代码
先判断了对象的finalize()是否已经执行过了,执行过了的不会再执行。
Object finalizee = this.get()
获取到关联的垃圾对象,jla.invokeFinalize(finalizee)
这行代码就是去实际执行垃圾对象的finalize()
方法。
小结:
-
类加载时会初始化 FinalizerThread
线程,这个线程任务就是从队列中取出Finalizer对象,并执行其关联垃圾对象的finalize() -
当一个对象被标记并且有必要执行finalize(),那么虚拟机会注册一个Finalizer对象,关联垃圾对象和队列,Finalizer对象会被 ReferenceHandler线程添加到这个队列中。 -
涉及到两个线程,但他们的作用不同。 ReferenceHandler负责将pending链表中的元素添加到对应的队列中,不关心怎么处理队列中元素; FinalizerThread负责从队列中取出元素进行处理,不关心元素怎么入队的。
实例分析
1. 实例分析
public class ReferenceMain {
private static ReferenceQueue<ReferenceObserveObject> referenceQueue = new ReferenceQueue<>();
public static void main(String[] args) throws InterruptedException {
ReferenceObserveObject o1 = new ReferenceObserveObject(1);
ReferenceObserveObject o2 = new ReferenceObserveObject(2);
ReferenceObserveObject o3 = new ReferenceObserveObject(3); WeakReference<ReferenceObserveObject> weakReference1 = new WeakReference<>(o1, referenceQueue); WeakReference<ReferenceObserveObject> weakReference2 = new WeakReference<>(o2, referenceQueue); WeakReference<ReferenceObserveObject> weakReference3 = new WeakReference<>(o3, referenceQueue); System.out.println(weakReference1); System.out.println(weakReference2); System.out.println(weakReference3); System.out.println(); // 将关联的对象置为null, help GC o1 = null; o2 = null; o3 = null; Thread.sleep(1000); // 通知GC回收 System.gc(); Reference<? extends ReferenceObserveObject> r; while (true) { r = referenceQueue.poll(); // 获取引用关联的对象 if( null != r){ ReferenceObserveObject roo = r.get(); System.out.println(r); System.out.println(roo); } } } @Data @AllArgsConstructor private static class ReferenceObserveObject { private int id; @Override public String toString() { return "ReferenceObserveObject{" + "id=" + id + '}'; } } } 复制代码
结果:
java.lang.ref.WeakReference@65b54208
java.lang.ref.WeakReference@1be6f5c3
java.lang.ref.WeakReference@6b884d57
java.lang.ref.WeakReference@6b884d57
null java.lang.ref.WeakReference@65b54208 null java.lang.ref.WeakReference@1be6f5c3 null 复制代码
从队列中取出的WeakReference引用与我们创建的一致,并且从中取出的关联对象都是NULL(已被回收)。
2.WeakHashMap
WeakHashMap经常用来缓存key-value形式的数据,用到了WeakReference。
WeakHashMap.Entry:
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); this.value = value; this.hash = hash; this.next = next; } } 复制代码
继承了WeakReference
,super(key, queue)
,每创建一个Entry实例,就相当于创建了一个WeakReference
对象,key对象就是关联的对象,当key指向的对象被回收时,这个Entry对象就会被加入全局的private final ReferenceQueue<Object> queue = new ReferenceQueue<>()
队列中。
加入到队列的是Entry对象
搜索一下就知道queue 用在expungeStaleEntries()方法中:
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
// 从队列中取出Entry实例,也就是这个Entry关联的key对象被GC回收了
Entry<K,V> e = (Entry<K,V>) x; int i = indexFor(e.hash, table.length); // Entry<K,V> prev = table[i]; Entry<K,V> p = prev; while (p != null) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // Must not null out e.next; // stale entries may be in use by a HashIterator e.value = null; // Help GC size--; break; } prev = p; p = next; } } } } 复制代码
ReferenceQueue中的Entry对象会被逐一取出,找到他们在table中的下标,然后删除。
本文使用 mdnice 排版