前言
Java在1.2版本之前只有普通的强引用,只要对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM抛出了OOME时,也不会去回收存在引用的对象。
如果只提供强引用,那我我们很难写出这个对象不是很重要,如果内存不足时,可以GC回收掉
这样的语义代码来。所幸的是,Java在1.2版本中完善了引体系,提供了四种引用类型: 强引用
,软引用
,弱引用
和虚引用
,我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作。
引用与可达性分类
Java目前有四种引用类型:
- 强引用(Strong Reference)
普通的引用类型,new一个对象默认得到的就是强引用类型,只要对象存在强引用,就不会被GC掉。 - 软引用(Soft Reference)
垃圾回收器会在内存不足时回收软引用指向的对象。JVM会在抛出OOME前清理所有软引用指向的对象,如果清理完还是内存不足,才会抛出OOME。所以软件引用一般用于实现内存敏感(memory-sensitive)的缓存设计。 - 弱引用(Weak Reference)
弱引用对象,它不阻止将其引用变成finalizable
、finalized
和reclaimed
。换一种说法,即垃圾回收器在GC时会回收些对象。 - 虚引用(Phantom Reference)
虚引用是一种比较特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。
使用可达性分析来判断一个对象是否存活,其基本的思路是从GC Root开始向下搜索,如果对象与GC Root之间存在引用链,则对象是可达的。对象的可达性与引用类型密切相关。
Java中有五种类型的可达性:
- 强可达(Strongly Reachable)
- 软可达(Soft Reachable)
- 弱可达(Weak Reachable)
- 虚可达(Phantom Reachable)
- 不可达(Unreachable)
对象的引用类型与可达性的对应关系,我们使用下面的例子来讲一下:
有5个对名,obj1~obj5, 每个对象只有一个引用,分别为:
obj1 - 强引用 ----> obj1 - 强可达;
obj2 - 软引用 ----> obj2 - 软可达;
obj3 - 弱引用 ----> obj3 - 弱可达;
obj4 - 虚引用 ----> obj4 - 虚可达;
obj5 - 无引用 ----> obj5 - 不可达 (obj5没有存在和GC Root的引用链,所以不可达);
Reference结构图
Reference的核心
Java中的多种引用类型的实现,不是通过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变更referent
,其他部分代码如下:
public abstract class Reference {
/**
* 一个引用的对象可能会有以下四种状态中的一种:
* Active: 新创建的对象实例状态即为Active状态。这个实例如果注册为队列,则进入Pending状态,否则进入Inactive状态。
* Pending: 未注册的实例不会到达这个状态;在pendingReference列表中的元素,等待Reference-handler线程enqueue操作
* Enqueued: 未注册的实例不会到达这个状态;当实例从ReferenceQueue列表中删除时,进入Inactive状态。
* Inactive: 一旦实例变为Inactive(非活动)状态,它的状态将不再更改。
*/
private T referent; /*被GC特别的处理*/
/**
* 返回引用管理的对象,如果这个对象已经被回收,则返回null.
*/
public T get() {
return this.referent;
}
}
我们从上文提到了,Reference类及其子类有两大功能:
- 实现了特定的引用类型;
对于这个功能点是怎么实现的呢?
如果JVM没有对referent
这个变量做特殊处理,它依然只是一个普通的强引用,之所以会出现不同的引用类型,是因为JVM垃圾回收器硬编码识别SoftReference
,WeakReference
,PhantomReference
等这些具体的类,对其reference变量进行特殊对象处理,才有了不同的引用类型的效果。 - 用户可以在对象被回收后得到通知;
看到这个问题,我们想到的首先想到的解决方案就是:在新建一个Reference实例时,添加一个回调,当java.lang.ref.Reference#referent
被回收时,JVM调用该回调。这种思路就是我们一般的通知模型,但是对于引用与垃圾回收这种底层场景来说,会导致实现复杂,性能不高的问题,比如需要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等。
而通过Reference的源码,我们了解到它使用了一种更加原始的方式来做通知,就是把引用对象被回收的Reference添加到一个队列中,用户后续自己从队列中获取并使用。
Reference相关部分代码如下:
public abstract class Reference {
// ...
// referent被回收后,当前Reference实例会被添加到这个队列中
volatile ReferenceQueue super T> queue;
// ...
// 只传入referent的构造函数,意味着用户只需要特殊的引用类型,不关心对象何时被GC
Reference(T referent) {
this(referent, null);
}
// 传入referent和ReferenceQueue的构造函数,referent被回收后,会添加到queue中
Reference(T referent, ReferenceQueue super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
// ...
}
Reference的状态
Reference对象是有状态的,一共有四种:
- Active - 新创建的实例状态,由垃圾回收器进行处理,如果实例的可达性(reachability)处于合适的状态,垃圾回收器会切换实例的状态为Pending或Inactive。如果Reference注册了ReferenceQueue,则切换为Pending,并且Reference会加入到pending-Reference链表中,如果没有注册ReferenceQueue,会切换为Inactive。
- Pending - 在pending-Reference链表中的Reference的状态,这些Reference等待被加入到ReferenceQueue中。
- Enqueued - 在ReferenceQueue队列中的Reference的状态,如果Reference从列表中移除,会进入Inactive状态。
- Inactive - Reference的最终状态,不可改变。
其状态图转换如下:
在Reference的四种状态中,我们看到有一个pending-Reference
链表,我们接下来探讨一下这个链表是用来干什么的。
在reference引用的对象被回收后,该Reference实例会被添加到ReferenceQueue中,但是这个不是垃圾回收器来做的,这个操作还是一定的复杂度的,如果垃圾回收器还要执行这个操作,就会降低其效率。
所以垃圾回收器做的是一个非常轻量级的操作:把Reference添加到pending-Reference链表中。Reference对象中有一个静态的pending成员变量,它就是这个pending-Reference链表的头结点。而另一个成员变量discovered
就是这个链表的指针,指向下一个节点。
相应的代码段如下:
public abstract class Reference {
// ...
/**
* Active: 由垃圾回收器管理的已发现的引用列表
* pending: 在pending列表中的下一个元素,如果没有为null
*/
transient private Reference discovered; /* used by VM */
// 全局唯一的pending-Reference列表
private static Reference
ReferenceHandler线程
在上面,我们已经知道一个Referenece实例化后的状态为Active
,当其引用的对象被回收之后,垃圾回收器将其加入到pending-Reference链表中,等待加入 ReferenceQueue。那这个过程是由谁来实现的呢?
这个过程不能对垃圾回收器产生影响,所以不能在垃圾回收线程中执行,也就需要一个独立的线程来负责,这个线程就是ReferenceHandler
,它的定义在Reference类中:
public abstract class Reference {
// ...
/**
* 用于控制垃圾回收器操作与Pending状态的Reference入队操作不冲突执行的全局锁
* 垃圾回收器开始一轮垃圾回收前要获取此锁
* 所以所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码
*/
static private class Lock { }
private static Lock lock = new Lock();
// 最高优先级的线程
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 {
// 预加载、初始化InterruptedException和Cleaner类,
// 这样防止在以后的运行循环中遇到延迟加载、初始化它们时出现内存不足的问题。
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
// 线程一直运行
while (true) {
tryHandlePending(true);
}
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference
ReferenceHandler线程是在Reference类中的static块中启动的:
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中提供访问权限
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
从上面的代码中我们知道,ReferenceHandler是一个最高优先级的线程,其逻辑是从Pending-Reference链表中取出Reference,添加到其关联的ReferenceQueue中。
ReferenceQueue
上面我们一直在引用ReferenceQueue,现在我们一起看一下ReferenceQueue类对象:
public class ReferenceQueue {
// 构造一个新的队列
public ReferenceQueue() { }
private static class Null extends ReferenceQueue {
boolean enqueue(Reference extends S> r) {
return false;
}
}
static ReferenceQueue