深入理解JDK中的Reference原理和源码实现
前提
这篇文章主要基于JDK11的源码和最近翻看的《深入理解Java虚拟机-2nd》一书的部分内容,对JDK11中的Reference
(引用)做一些总结。值得注意的是,通过笔者对比一下JDK11和JDK8对于java.lang.ref
包的相关实现,发现代码变化比较大,因此本文的源码分析可能并不适合于JDK11之外的JDK版本。
Reference的简介和分类
在JDK1.2之前,Java中的引用的定义是十分传统的:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。在这种定义之下,一个对象只有被引用和没有被引用两种状态。
实际上,我们更希望存在这样的一类对象:当内存空间还足够的时候,这些对象能够保留在内存空间中;如果当内存空间在进行了垃圾收集之后还是非常紧张,则可以抛弃这些对象。基于这种特性,可以满足很多系统的缓存功能的使用场景。
java.lang.ref
包是JDK1.2引入的,包结构和类分布如下:
- java.lang.ref
- Cleaner.class
- Finalizer.class
- FinalizerHistogram.class
- FinalReference.class
- PhantomReference.class
- Reference.class
- ReferenceQueue.class
- SoftReference.classs
- WeakReference.class
引入此包的作用是对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种类型的引用,还有一种比较特殊的引用是析构引用(Final Reference),它是一种特化的虚引用。四种引用的强度按照下面的次序依次减弱:
StrongReference > SoftReference > WeakReference > PhantomReference
值得注意的是:
- 强引用没有对应的类型表示,也就是说强引用是普遍存在的,如
Object object = new Object();
。 - 软引用、弱引用和虚引用都是
java.lang.ref.Reference
的直接子类。 - 直到JDK11为止,只存在四种引用,这些引用是由JVM创建,因此直接继承
java.lang.ref.Reference
创建自定义的引用类型是无效的,但是可以直接继承已经存在的引用类型,如java.lang.ref.Cleaner
就是继承自java.lang.ref.PhantomReference
。 - 特殊的
java.lang.ref.Reference
的子类java.lang.ref.FinalReference
和Object#finalize()
有关,java.lang.ref.Finalizer
是java.lang.ref.FinalReference
子类,下文会详细分析这些内容。
Reference
Reference
就是引用,对JVM的垃圾收集活动敏感(当然,强引用可能对垃圾收集活动是不敏感的),Reference
的继承关系或者实现是由JDK定制,引用实例是由JVM创建,所以自行继承Reference
实现自定义的引用类型是无意义的,但是可以继承已经存在的引用类型,如SoftReference
等。Reference
类文件的注释也比较简短,但是方法和变量的注释十分详细,特别是用图表表明了状态跃迁的过程,这里先看类文件头注释:
Abstract base class for reference objects. This class defines the operations common to all reference objects. Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.
翻译一下大意是:Reference
是所有引用对象的基类。这个类定义了所有引用对象的通用操作。因为引用对象是与垃圾收集器紧密协作而实现的,所以这个类可能不能直接子类化。
Reference的状态集合
Reference
源码中并不存在一个成员变量用于描述Reference
的状态,它是通过组合判断referent、discovered、queue、next成员的存在性或者顺序"拼凑出"对应的状态,注释中描述如下:
一个引用对象可以同时存在两种状态:
- 第一组状态:"active", "pending", or "inactive"
- 第二组状态:"registered", "enqueued", "dequeued", or "unregistered"
Active:
当前引用实例处于Active状态,会收到垃圾收集器的特殊处理。在垃圾收集器检测到referent的可达性已更改为适当状态之后的某个时间,垃圾收集器会"通知"当前引用实例改变其状态为"pending"或者"inactive"。此时的判断条件是:referent != null; discovered = null或者实例位于GC的discovered列表中。
Pending:
当前的引用实例是pending-Reference列表的一个元素,等待被ReferenceHandler线程处理。pending-Reference列表通过应用实例的discovered字段进行关联。此时的判断条件是:referent = null; discovered = pending-Reference列表中的下一个元素
Inactive:
当前的引用实例处于非Active和非Pending状态。此时的判断条件是:referent = null (同时discovered = null)
Registered:
当前的引用实例创建的时候关联到一个引用队列实例,但是引用实例暂未加入到队列中。此时的判断条件是:queue = 传入的ReferenceQueue实例
Enqueued:
当前的引用实例已经添加到和它关联的引用队列中但是尚未移除(remove),也就是调用了ReferenceQueue.enqueued()后的Reference实例就会处于这个状态。此时的判断条件是:queue = ReferenceQueue.ENQUEUE; next = 引用列表中的下一个引用实例,或者如果当前引用实例是引用列表中的最后一个元素,则它会进入Inactive状态
Dequeued:
当前的引用实例曾经添加到和它关联的引用队列中并且已经移除(remove)。此时的判断条件是:queue = ReferenceQueue.NULL; next = 当前的引用实例
Unregistered:
当前的引用实例不存在关联的引用队列,也就是创建引用实例的时候传入的queue为null。此时的判断条件是:queue = ReferenceQueue.NULL
状态跃迁的时序图如下:
* Initial states:
* [active/registered]
* [active/unregistered] [1]
*
* Transitions:
* clear
* [active/registered] -------> [inactive/registered]
* | |
* | | enqueue [2]
* | GC enqueue [2] |
* | -----------------|
* | |
* v |
* [pending/registered] --- v
* | | ReferenceHandler
* | enqueue [2] |---> [inactive/enqueued]
* v | |
* [pending/enqueued] --- |
* | | poll/remove
* | poll/remove |
* | |
* v ReferenceHandler v
* [pending/dequeued] ------> [inactive/dequeued]
*
*
* clear/enqueue/GC [3]
* [active/unregistered] ------
* | |
* | GC |
* | |--> [inactive/unregistered]
* v |
* [pending/unregistered] ------
* ReferenceHandler
*
* Terminal states:
* [inactive/dequeued]
* [inactive/unregistered]
*
* Unreachable states (because enqueue also clears):
* [active/enqeued]
* [active/dequeued]
*
* [1] Unregistered is not permitted for FinalReferences.
*
* [2] These transitions are not possible for FinalReferences, making
* [pending/enqueued] and [pending/dequeued] unreachable, and
* [inactive/registered] terminal.
*
* [3] The garbage collector may directly transition a Reference
* from [active/unregistered] to [inactive/unregistered],
* bypassing the pending-Reference list.
注释中还强调了几点:
- 初始化状态:
[active/registered]
和[active/unregistered](这种情况只限于FinalReferences)
。 - 终结状态:
[inactive/dequeued]
和[inactive/unregistered]
。 - 不可能出现的状态:
[active/enqeued]
和[active/dequeued]
。
上面的图看起来可能比较抽象,ReferenceHandler
其实是Reference
中静态代码块中初始化的线程实例,主要作用是:处理pending状态的引用实例,使它们入队列并走向[inactive/dequeued]
状态。另外,上面的线框图是分两部分,其中上半部分是使用了ReferenceQueue
,后半部分是没有使用ReferenceQueue
(或者说使用了ReferenceQueue.NULL
)。这里尝试用PPT画一下简化的状态跃迁图:
Reference源码分析
先看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成员以及一个ReferenceQueue<? super T>
的队列,如果ReferenceQueue
实例为null,则使用ReferenceQueue.NULL
。
成员变量描述:
- referent:
Reference
保存的引用指向的对象,下面直接称为referent。
// GC特殊处理的对象
private T referent; /* Treated specially by GC */
- queue:
Reference
对象关联的队列,也就是引用队列,对象如果即将被垃圾收集器回收,此队列作为通知的回调队列,也就是当Reference
实例持有的对象referent要被回收的时候,Reference
实例会被放入引用队列,那么程序执行的时候可以从引用队列得到或者监控相应的Reference
实例。
/* The queue this reference gets enqueued to by GC notification or by
* calling enqueue().
*
* When registered: the queue with which this reference is registered.
* enqueued: ReferenceQueue.ENQUEUE
* dequeued: ReferenceQueue.NULL
* unregistered: ReferenceQueue.NULL
*/
volatile ReferenceQueue<? super T> queue;
- next:下一个
Reference
实例的引用,Reference
实例通过此构造单向的链表。
/* The link in a ReferenceQueue's list of Reference objects.
*
* When registered: null
* enqueued: next element in queue (or this if last)
* dequeued: this (marking FinalReferences as inactive)
* unregistered: null
*/
@SuppressWarnings("rawtypes")
volatile Reference next;
- discovered:注意这个属性由transient修饰,基于状态表示不同链表中的下一个待处理的对象,主要是pending-reference列表的下一个元素,通过JVM直接调用赋值。
/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
实例方法(和ReferenceHandler线程不相关的方法):
// 获取持有的referent实例
@HotSpotIntrinsicCandidate
public T get() {
return this.referent;
}
// 把持有的referent实例置为null
public void clear() {
this.referent = null;
}
// 判断是否处于enqeued状态
public boolean isEnqueued() {
return (this.queue == ReferenceQueue.ENQUEUED);
}
// 入队参数,同时会把referent置为null
public boolean enqueue() {
this.referent = null;
return this.queue.enqueue(this);
}
// 覆盖clone方法并且抛出异常,也就是禁止clone
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
// 确保给定的引用实例是强可达的
@ForceInline
public static void reachabilityFence(Object ref) {
}
ReferenceHandler线程
ReferenceHandler线程是由Reference
静态代码块中建立并且运行的线程,它的运行方法中依赖了比较多的本地(native)方法,ReferenceHandler线程的主要功能是处理pending链表中的引用对象:
// ReferenceHandler直接继承于Thread覆盖了run方法
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这个类已经初始化
// pre-load and initialize Cleaner class so that we don't
// get into trouble later in the run loop if there's
// memory shortage while loading/initializing it lazily.
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, null, name, 0, false);
}
// 注意run方法是一个死循环执行processPendingReferences
public void run() {
while (true) {
processPendingReferences();
}
}
}
/* 原子获取(后)并且清理VM中的pending引用链表
* Atomically get and clear (set to null) the VM's pending-Reference list.
*/
private static native Reference<Object> getAndClearReferencePendingList();
/* 检验VM中的pending引用对象链表是否有剩余元素
* Test whether the VM's pending-Reference list contains any entries.
*/
private static native boolean hasReferencePendingList();
/* 等待直到pending引用对象链表不为null,此方法阻塞的具体实现又VM实现
* Wait until the VM's pending-Reference list may be non-null.
*/
private static native void waitForReferencePendingList();
// 锁对象,用于控制等待pending对象时候的加锁和开始处理这些对象时候的解锁
private static final Object processPendingLock = new Object();
// 正在处理pending对象的时候,这个变量会更新为true,处理完毕或者初始化状态为false,用于避免重复处理或者重复等待
private static boolean processPendingActive = false;
// 这个是死循环中的核心方法,功能是处理pending链表中的引用元素
private static void processPendingReferences() {
// Only the singleton reference processing thread calls
// waitForReferencePendingList() and getAndClearReferencePendingList().
// These are separate operations to avoid a race with other threads
// that are calling waitForReferenceProcessing().
// (1)等待
waitForReferencePendingList();
Reference<Object> pendingList;
synchronized (processPendingLock) {
// (2)获取并清理,标记处理中状态
pendingList = getAndClearReferencePendingList();
processPendingActive = true;
}
// (3)通过discovered(下一个元素)遍历pending链表进行处理
while (pendingList != null) {
Reference<Object> ref = pendingList;
pendingList = ref.discovered;
ref.discovered = null;
// 如果是Cleaner类型执行执行clean方法并且对锁对象processPendingLock进行唤醒所有阻塞的线程
if (ref instanceof Cleaner) {
((Cleaner)ref).clean();
// Notify any waiters that progress has been made.
// This improves latency for nio.Bits waiters, which
// are the only important ones.
synchronized (processPendingLock) {
processPendingLock.notifyAll();
}
} else {
// 非Cleaner类型并且引用队列不为ReferenceQueue.NULL则进行入队操作
ReferenceQueue<? super Object> q = ref.queue;
if (q != ReferenceQueue.NULL) q.enqueue(ref);
}
}
// (4)当次循环结束之前再次唤醒锁对象processPendingLock上阻塞的所有线程
// Notify any waiters of completion of current round.
synchronized (processPendingLock) {
processPendingActive = false;
processPendingLock.notifyAll();
}
}
ReferenceHandler线程启动的静态代码块如下:
static {
// ThreadGroup继承当前执行线程(一般是主线程)的线程组
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 创建线程实例,命名为Reference Handler,配置最高优先级和后台运行(守护线程),然后启动
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 注意这里覆盖了全局的jdk.internal.misc.JavaLangRefAccess实现
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean waitForReferenceProcessing()
throws InterruptedException{
return Reference.waitForReferenceProcessing();
}
@Override
public void runFinalization() {
Finalizer.runFinalization();
}
});
}
// 如果正在处理pending链表中的引用对象或者监测到VM中的pending链表中还有剩余元素则基于锁对象processPendingLock进行等待
private static boolean waitForReferenceProcessing()
throws InterruptedException{
synchronized (processPendingLock) {
if (processPendingActive || hasReferencePendingList()) {
// Wait for progress, not necessarily completion.
processPendingLock.wait();
return true;
} else {
return false;
}
}
}
由于ReferenceHandler线程是Reference
的静态代码创建的,所以只要Reference
这个父类被初始化,该线程就会创建和运行,由于它是守护线程,除非JVM进程终结,否则它会一直在后台运行(注意它的run()
方法里面使用了死循环)。
ReferenceQueue
JDK中对ReferenceQueue
的文档描述是比较少的,类文件只有一句简单的注释:
Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
翻译一下大意为:引用队列,垃圾收集器在检测到适当的可达性更改后将已注册的引用对象追加到该队列。
从源码上看,实际上ReferenceQueue
只是名义上的引用队列,它只保存了Reference
链表的头(head)节点,并且提供了出队、入队和移除等操作,而Reference
实际上本身提供单向链表的功能,也就是Reference
通过成员属性next构建单向链表,而链表的操作是委托给ReferenceQueue
完成,这里的逻辑有点绕。ReferenceQueue
的源码比较少,这里全量贴出标注一下注释:
public class ReferenceQueue<T> {
public ReferenceQueue() { }
// 内部类Null类继承自ReferenceQueue,覆盖了enqueue方法返回false
private static class Null extends ReferenceQueue<Object> {
boolean enqueue(Reference<?> r) {
return false;
}
}
// ReferenceQueue.NULL和ReferenceQueue.ENQUEUED都是内部类Null的新实例
static final ReferenceQueue<Object> NULL = new Null();
static final ReferenceQueue<Object> ENQUEUED = new Null();
// 静态内部类,作为锁对象
private static class Lock { };
// 锁实例
private final Lock lock = new Lock();
// 引用链表的头节点
private volatile Reference<? extends T> head;
// 引用队列长度,入队则增加1,出队则减少1
private long queueLength = 0;
// 入队操作,只会被Reference实例调用
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
// 加锁
synchronized (lock) {
// Check that since getting the lock this reference hasn't already been
// enqueued (and even then removed)
// 如果引用实例持有的队列为ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED则入队失败返回false
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// Self-loop end, so if a FinalReference it remains inactive.
// 如果链表没有元素,则此引用实例直接作为头节点,否则把前一个引用实例作为下一个节点
r.next = (head == null) ? r : head;
// 当前实例更新为头节点,也就是每一个新入队的引用实例都是作为头节点,已有的引用实例会作为后继节点
head = r;
// 队列长度增加1
queueLength++;
// Update r.queue *after* adding to list, to avoid race
// with concurrent enqueued checks and fast-path poll().
// Volatiles ensure ordering.
// 当前引用实例已经入队,那么它本身持有的引用队列实例置为ReferenceQueue.ENQUEUED
r.queue = ENQUEUED;
// 特殊处理FinalReference,VM进行计数
if (r instanceof FinalReference) {
VM.addFinalRefCount(1);
}
// 唤醒所有等待的线程
lock.notifyAll();
return true;
}
}
// 引用队列的poll操作,此方法必须在加锁情况下调用
private Reference<? extends T> reallyPoll() { /* Must hold lock */
Reference<? extends T> r = head;
if (r != null) {
r.queue = NULL;
// Update r.queue *before* removing from list, to avoid
// race with concurrent enqueued checks and fast-path
// poll(). Volatiles ensure ordering.
@SuppressWarnings("unchecked")
Reference<? extends T> rn = r.next;
// Handle self-looped next as end of list designator.
// 更新next节点为头节点,如果next节点为自身,说明已经走过一次出队,则返回null
head = (rn == r) ? null : rn;
// Self-loop next rather than setting to null, so if a
// FinalReference it remains inactive.
// 当前头节点变更为环状队列,考虑到FinalReference尚为inactive和避免重复出队的问题
r.next = r;
// 队列长度减少1
queueLength--;
// 特殊处理FinalReference,VM进行计数
if (r instanceof FinalReference) {
VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
// 队列的公有poll操作,主要是加锁后调用reallyPoll
public Reference<? extends T> poll() {
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
// 移除引用队列中的下一个引用元素,实际上也是依赖于reallyPoll的Object提供的阻塞机制
public Reference<? extends T> remove(long timeout)
throws IllegalArgumentException, InterruptedException{
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value");
}
synchronized (lock) {
Reference<? extends T> r = reallyPoll();
if (r != null) return r;
long start = (timeout == 0) ? 0 : System.nanoTime();
for (;;) {
lock.wait(timeout);
r = reallyPoll();
if (r != null) return r;
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null;
start = end;
}
}
}
}
// remove,超时时间为0,实际上就是lock.wait(0)就是永久阻塞直至唤醒
public Reference<? extends T> remove() throws InterruptedException {
return remove(0);
}
// foreach
void forEach(Consumer<? super Reference<? extends T>> action) {
for (Reference<? extends T> r = head; r != null;) {
action.accept(r);
@SuppressWarnings("unchecked")
Reference<? extends T> rn = r.next;
if (rn == r) {
if (r.queue == ENQUEUED) {
// still enqueued -> we reached end of chain
r = null;
} else {
// already dequeued: r.queue == NULL; ->
// restart from head when overtaken by queue poller(s)
r = head;
}
} else {
// next in chain
r = rn;
}
}
}
}
ReferenceQueue
的源码十分简单,还是重新提一下,它只存储了Reference
链表的头节点,真正的Reference
链表的所有节点是存储在Reference
实例本身,通过属性next拼接的,ReferenceQueue
提供了对Reference
链表的入队、poll、remove等操作。
判断对象的可达性和对象是否存活
判断对象的可达性和对象是否存活是两个比较困难的问题,笔者C语言学得比较烂,否则会重点翻看一下JVM的实现,目前只能参考一些资料来说明这个问题。
可达性算法
主流商用语言包括Java都是使用可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路是通过一系列的称为"GC Roots"(GC根集)的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC根集没有任何引用链相连(从图论的角度看,也就是从GC根集到这个对象是不可达的)时,则证明此对象是不可用的。不可用的对象"有机会"被判定为可以回收的对象。
在Java语言中,可以作为GC根集的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中常量引用的对象(在JDK1.8之后不存在方法区,也就是有可能是metaspace中常量引用的对象)。
- 本地方法栈中JNI(即一般常说的Native方法)引用的对象。
finalize函数
即使在可达性分析算法中判定为不可达的对象,也并非一定会判定为可以被回收的"死亡"对象。一个对象判定为"死亡"至少需要经历两次标记的过程。
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法。JVM会把以下两种情况认为对象没有必要执行finalize()
方法:
- 对象没有覆盖继承自Object类的
finalize()
方法。 - 对象的
finalize()
方法已经被JVM调用过。
如果一个对象被判定为有必要执行finalize()
方法,那么这个对象将会被放置在一个叫F-Queue
的队列之中,并且稍后由一个优先级低的Finalizer线程去取该队列的元素,"尝试执行"元素的finalize()
方法。这里之所以叫尝试执行是因为JVM会保证触发满足条件的对象的finalize()
方法,但是并不承诺会等待它执行结束,这是因为:如果一个对象在执行finalize()
方法耗时较长,甚至发生了死循环,将会导致F-Queue
的队列中的其他元素永远处于等待状态,极端情况下有可能导致整个内存回收系统崩溃。
finalize()
方法是对象逃脱死亡命运的最后一次机会,因为稍后的GC将会对F-Queue
队列中的对象进行第二次小规模的标记,如果对象在finalize()
方法执行过程中成功拯救自己--也就是对象自身重新与引用链的任何一个对象建立关联即可,最常见的就是把自身(this关键字)赋值给某个类变量或者对象的成员属性,那么在第二次小规模的标记时候将会把"自我拯救"成功的对象移出"即将回收"的集合。如果对象在finalize()
方法执行过程中没有"逃逸",那么它最终就会被回收。参考《深入理解Java虚拟机-2nd》的"对象自我拯救的例子":
public class FinalizeEscapeGc {
private static FinalizeEscapeGc SAVE_HOOK = null;
public void isAlive() {
System.out.println("Yes,I am still alive :)");
}
public static void main(String[] args) throws Exception {
SAVE_HOOK = new FinalizeEscapeGc();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No,I am not alive :(");
}
// 下面的这段代码和上面的一致
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No,I am not alive :(");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("FinalizeEscapeGc finalize invoke...");
FinalizeEscapeGc.SAVE_HOOK = this;
}
}
// 输出结果
FinalizeEscapeGc finalize invoke...
Yes,I am still alive :)
No,I am not alive :(
注意:
finalize()
方法的错误使用有可能是内存回收系统崩溃的根源,一般情况下谨慎思考是否真的需要覆盖此方法。- 任意一个对象只能通过
finalize()
方法自我拯救一次。
Finalizer守护线程
前面提到的Finalizer
守护线程和F-Queue
队列其实在JDK中有具体的实现类java.lang.ref.Finalizer
。F-Queue
队列只是《深入理解Java虚拟机-2nd》中的一个名词描述,实际上笔者没有找到相关的资料,这里我们通过分析JDK和JVM相关的源码去理解这个F-Queue
队列吧。先看java.lang.ref.Finalizer
的源码,代码比较少全量贴出:
final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
same package as the Reference
class */
// Finalizer关联的ReferenceQueue,其实Finalizer是一个特殊的Reference实现
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
/** Head of doubly linked list of Finalizers awaiting finalization. */
// 等待finalization的所有Finalizer实例链表的头节点,这里称此链表为unfinalized链表
private static Finalizer unfinalized = null;
/** Lock guarding access to unfinalized list. */
// unfinalized链表的锁,静态final,全局的锁实例
private static final Object lock = new Object();
// 中间变量,分别记录unfinalized链表中当前执行元素的下一个节点和前一个节点
private Finalizer next, prev;
private Finalizer(Object finalizee) {
super(finalizee, queue);
// push onto unfinalized
// 这里主要是更新unfinalized链表的头节点,新增的元素总是会变成头节点
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
static ReferenceQueue<Object> getQueue() {
return queue;
}
/* Invoked by VM */ 这个方法由JVM激活,也就是链表的元素入队是由JVM控制的,见下文分析
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private void runFinalizer(JavaLangAccess jla) {
synchronized (lock) {
// 当前元素已经处理过,直接返回
if (this.next == this) // already finalized
return;
// unlink from unfinalized
// 下面的逻辑是当前需要执行的元素从链表中移除,并且更新prev和next的值,相当于重建链表的部分节点
if (unfinalized == this)
unfinalized = this.next;
else
this.prev.next = this.next;
if (this.next != null)
this.next.prev = this.prev;
this.prev = null;
this.next = this; // mark as finalized
}
try {
// 获取对象执行一次finalize方法
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
// Clear stack slot containing this variable, to decrease
// the chances of false retention with a conservative GC
// 清空变量引用从而减少保守GC导致变量保留的可能性
finalizee = null;
}
} catch (Throwable x) { }
// 执行完毕会做一次情况防止重复执行
super.clear();
}
/* Create a privileged secondary finalizer thread in the system thread
* group for the given Runnable, and wait for it to complete.
*
* This method is used by runFinalization.
*
* It could have been implemented by offloading the work to the
* regular finalizer thread and waiting for that thread to finish.
* The advantage of creating a fresh thread, however, is that it insulates
* invokers of that method from a stalled or deadlocked finalizer thread.
*/
// 这里其实不用畏惧注释太多,它只是一个候选方法,新建一个线程直接调用包裹在Runnable的runFinalization方法,主要是提供给主动调用的上层方法调用的
private static void forkSecondaryFinalizer(final Runnable proc) {
AccessController.doPrivileged(
new PrivilegedAction<>() {
public Void run() {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread sft = new Thread(tg, proc, "Secondary finalizer", 0, false);
sft.start();
try {
sft.join();
} catch (InterruptedException x) {
Thread.currentThread().interrupt();
}
return null;
}});
}
/* Called by Runtime.runFinalization() */
// 这个方法是给Runtime.runFinalization()委托调用的,其实就是主动取出queue的元素强制调用其finalize方法
static void runFinalization() {
if (VM.initLevel() == 0) {
return;
}
forkSecondaryFinalizer(new Runnable() {
private volatile boolean running;
public void run() {
// in case of recursive call to run()
if (running)
return;
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (Finalizer f; (f = (Finalizer)queue.poll()) != null;)
f.runFinalizer(jla);
}
});
}
// 真正的Finalizer线程
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, null, "Finalizer", 0, false);
}
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.initLevel() == 0) {
// delay until VM completes initialization
try {
VM.awaitInitLevel(1);
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
// 注意这里是死循环
for (;;) {
try {
// 注意这里是调用`Reference#remove()`的永久阻塞版本,只有`Reference#enqueue()`被调用才会解除阻塞
// `Reference#remove()`解除阻塞说明元素已经完成入队,由ReferenceHandler线程完成
Finalizer f = (Finalizer)queue.remove();
// 实际上就是调用对象的finalize方法
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 静态代码块中声明线程,优先级是最高优先级-2,守护线程,实际上这里优先级不一定会生效
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
}
上面的注释已经很明显标注出来,这里小结一下内容。
Finalizer
是FinalReference
的子类,而FinalReference
是Reference
的实现,所以它的工作原理和其他引用类似,对象的状态更变和由ReferenceHandler线程密切相关。Finalizer
内部维护了一个链表,每当JVM调用静态注册方法就会新建一个Finalizer
实例加入到链表的头节点中,头节点元素为unfinalized,这里称此链表为unfinalized链表。Finalizer
线程由Finalizer
静态代码块构建并且运行,它是守护线程,优先级是最高优先级-2,它的作用就是提取unfinalized链表的元素并且执行元素对象的finalize()
方法,过程中还会涉及到线程的阻塞、唤醒,以及unfinalized链表的重建等工作。
由于静态方法Finalizer#register(Object finalizee)
是由JVM调用的,所以我们必须要分析一些JVM的源码,参考的是OpenJDK主分支的代码,文件是instanceKlass.cpp
:
instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) {
if (TraceFinalizerRegistration) {
tty->print("Registered ");
i->print_value_on(tty);
tty->print_cr(" (" INTPTR_FORMAT ") as finalizable", p2i(i));
}
instanceHandle h_i(THREAD, i);
// Pass the handle as argument, JavaCalls::call expects oop as jobjects
JavaValue result(T_VOID);
JavaCallArguments args(h_i);
// 这里Universe::finalizer_register_method()获取到的就是Finalizer#register方法句柄
methodHandle mh (THREAD, Universe::finalizer_register_method());
JavaCalls::call(&result, mh, &args, CHECK_NULL);
return h_i();
}
最后调用的是javaCalls.cpp
:
void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
// Check if we need to wrap a potential OS exception handler around thread
// This is used for e.g. Win32 structured exception handlers
assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
// Need to wrap each and every time, since there might be native code down the
// stack that has installed its own exception handlers
os::os_exception_wrapper(call_helper, result, method, args, THREAD);
}
简单来看就是把创建对象过程中,如果有必要注册Finalizer
(一般是覆盖了finalize()
方法),则基于当前线程通过Finalizer#register(Object finalizee)
把当前新建的实例注册到Finalizer
自身维护的链表中(如果没理解错,所谓的F-Queue
就是这个链表了),等待后台Finalizer
线程轮询并且执行链表中对象的finalize()
方法。
各类引用以及它们的使用场景
这里提到的各类引用目前就是四种:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。其实还有特殊的引用类型FinalReference
,它是包私有的,并且只有一个子类型Finalizer
。
StrongReference
StrongReference也就是强引用,它是使用最普遍的一种引用,java.lang.ref
包下没有强引用对应的类型。一个比较明确的强引用定义就是:所有和GC Root之间存在引用链的对象都具备强引用。举个简单例子:形如Object o = new Object();
在方法体中使用new关键字声明的对象一般就是强引用。如果一个对象具备强引用,垃圾回收器绝不会回收它。当内存空间不足,JVM宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会出现回收具有强引用的对象来解决内存不足的情况。当然,如果有共享的成员变量在方法退出之前置为null,相当于断绝成员变量和GC Root的引用链,在合适的时机是有利于GC后具备强引用的对象的回收,例如:
private Object shareValue = XXX;
public void methodA(){
//do something
shareValue = null;
}
后来有人过度信奉类似上面的这个实践,出现了一条比较诡异的编码实践:强引用使用完毕后都要置为null方便对象回收。但是实际上,这个实践并不是在任何场景都是合理的。
SoftReference
SoftReference也就是软引用,它是用来描述一些"还有用但是非必须"的对象。对于软引用关联着的对象,在JVM应用即将发生内存溢出异常之前,将会把这些软引用关联的对象列进去回收对象范围之中进行第二次回收。如果这次回收之后还是没有足够的内存,才会抛出内存溢出异常。简单来说就是:
- 如果内存空间足够,垃圾回收器就不会回收软引用关联着的对象。
- 如果内存空间不足,垃圾回收器在将要抛出内存溢出异常之前会回收软引用关联着的对象。
举个简单的使用例子:
// VM参数:-Xmx4m -Xms4m
public class SoftReferenceMain {
public static void main(String[] args) throws Exception {
ReferenceQueue<SoftReferenceObject> queue = new ReferenceQueue<>();
SoftReferenceObject object = new SoftReferenceObject();
SoftReference<SoftReferenceObject> reference = new SoftReference<>(object, queue);
object = null;
System.gc();
Thread.sleep(500);
System.out.println(reference.get());
}
private static class SoftReferenceObject {
int[] array = new int[120_000];
@Override
public String toString() {
return "SoftReferenceObject";
}
}
}
// 运行后输出结果
null
上面的例子故意把JVM的启动的最大Heap内存和初始Heap内存设置为4MB,使用这个对象初始化一个比较大的整型数组并且关系到一个软引用对象中,GC之后,发现软引用关联的对象被回收了。
WeakReference
WeakReference也就是弱引用,弱引用和软引用类似,它是用来描述"非必须"的对象的,它的强度比软引用要更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前,简言之就是:一旦发生GC必定回收被弱引用关联的对象,不管当前的内存是否足够。
举个例子:
public class WeakReferenceMain {
public static void main(String[] args) throws Exception {
ReferenceQueue<WeakReferenceObject> queue = new ReferenceQueue<>();
WeakReferenceObject object = new WeakReferenceObject();
System.out.println(object);
WeakReference<WeakReferenceObject> reference = new WeakReference<>(object, queue);
object = null;
System.gc();
Thread.sleep(500);
System.out.println(reference.get());
}
private static class WeakReferenceObject {
@Override
public String toString() {
return "WeakReferenceObject";
}
}
}
// 运行后输出结果
WeakReferenceObject
null
上面的例子中没有设定JVM的堆内存,因此不存在内存不足的情况,可见弱引用关联的对象在GC之后被回收了。弱引用适合用来做对内存敏感的缓存,很常用的WeakHashMap
就是基于弱引用实现的。
PhantomReference
PhantomReference也就是虚引用,也叫幽灵引用或者幻影引用,它是所有引用类型中最弱的一种。一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例(PhantomReference
覆盖了Reference#get()
并且总是返回null)。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个系统通知。PhantomReference
有两个比较常用的子类是java.lang.ref.Cleaner
和jdk.internal.ref.Cleaner
,其中前者提供的功能是开发者用于在引用对象回收的时候触发一个动作(java.lang.ref.Cleaner$Cleanable
),后者用于DirectByteBuffer
对象回收的时候对于堆外内存的回收,可以翻看前面描述java.lang.ref.Reference#processPendingReferences()
源码的时候,ReferenceHandler线程会对pending链表中的jdk.internal.ref.Cleaner
类型引用对象调用其clean()
方法。PhantomReference
本身使用场景比较少,这里举一下java.lang.ref.Cleaner
注释中的例子:
public class PhantomReferenceMain {
public static void main(String[] args) throws Exception {
try (CleaningExample o = new CleaningExample(11)){
}
CleaningExample o2 = new CleaningExample(22);
System.gc();
Thread.sleep(300);
}
}
class CleaningExample implements AutoCloseable {
private Cleaner cleaner = Cleaner.create();
private final State state;
private final Cleaner.Cleanable cleanable;
public CleaningExample(int s) {
state = new State(s);
cleanable = cleaner.register(this, state);
}
class State implements Runnable {
private final int s;
public State(int s) {
this.s = s;
}
@Override
public void run() {
System.out.println("State runnable in action.State value = " + s);
}
}
@Override
public void close() throws Exception {
cleanable.clean();
}
}
实际上,沙面的代码执行完毕只会输出"State runnable in action.State value = 11",并没有输出"State runnable in action.State value = 22",这是因为无法预测强引用对象被回收的时机。java.lang.ref.Cleaner
主要是用于预防实现了AutoCloseable
接口的实例忘记调用close()
方法在对象被垃圾收集器回收的时候(内存回收)做一个兜底的清理工作,在JDK9之后,java.lang.ref.Cleaner
主要是为了替代已经标识为过期的Object#finalize()
方法。
扩展阅读:可以注意阅读一下《Effective Java 3rd》的第8小节,摘抄部分内容如下:终结方法(Finalizer)是不可预知的,很多时候是危险的,而且一般情况下是不必要的。...在Java 9中,终结方法已经被遗弃了,但它们仍被Java类库使用,相应用来替代终结方法的是清理方法(cleaner)。比起终结方法,清理方法相对安全点,但仍是不可以预知的,运行慢的,而且一般情况下是不必要的。
JDK9中有很多原来使用覆盖Object#finalize()
方法的清理工作实现都替换为java.lang.ref.Cleaner
,但是仍然不鼓励使用这种方式。
Reference和ReferenceQueue配合使用
前面基本介绍完了所有类型引用以及相关的源码,但是尚未提供例子说明Reference
和ReferenceQueue
是怎么配合使用的。举个例子:
public class ReferenceQueueMain {
public static void main(String[] args) throws Exception {
ReferenceQueue<WeakReferenceObject> queue = new ReferenceQueue<>();
WeakReferenceObject object = new WeakReferenceObject();
WeakReference<WeakReferenceObject> weakReference = new WeakReference<>(object, queue);
System.out.println(weakReference);
object = null;
System.gc();
Thread.sleep(500);
while (true) {
Reference<? extends WeakReferenceObject> reference = queue.poll();
if (null == reference) {
Thread.sleep(100);
} else {
System.out.println(reference);
System.out.println(reference.get());
break;
}
}
}
private static class WeakReferenceObject {
@Override
public String toString() {
return "WeakReferenceObject";
}
}
}
运行后输出结果是:
java.lang.ref.WeakReference@6537cf78
java.lang.ref.WeakReference@6537cf78
null
可见轮询ReferenceQueue
实例得到的弱引用实例和创建的是一致的,只是它持有的关联的对象已经被回收,得到null。上面的ReferenceQueue#poll()
方法也可以替换为ReferenceQueue#remove()
,这样子就不用写在死循环中,因为ReferenceQueue#remove()
会阻塞到有元素可以出队。通过轮询绑定到Reference
实例的ReferenceQueue
实例,就可以得知Reference
实例当前的状态并且判断它关联的我们真正关注的对象是否被回收。
小结
Reference
是非强引用的其他三种引用的共同父类。ReferenceQueue
只存储了引用链表的头节点,提供了引用链表的操作,实际上,引用链表是Reference
实例内部变量存储的。- ReferenceHandler守护线程线程由
Reference
的静态代码块创建和运行,作用是处理pending链表的引用元素使之状态变更,伴随着ReferenceQueue
的相关操作。 - Finalizer守护线程是由
Finalizer
类的静态代码块创建和运行的,作用是处理Finalizer
类内部维护的F-Queue链表(链表元素入队操作由JVM实现)的元素调用关联对象的finalize()
方法。 - ReferenceHandler守护线程线和Finalizer守护线程共同协作才能使引用类型对象内存回收系统的工作能够正常进行。
四种引用类型的总结:
引用类型 | 被垃圾收集器回收的时机 | 主要用途 | 生存周期 |
---|---|---|---|
强引用 | 直到内存溢出也不会回收 | 普遍对象的状态 | 从创建到JVM实例终止运行 |
软引用 | 垃圾回收并且内存不足时 | 有用但非必须的对象缓存 | 从创建到垃圾回收并且内存不足时 |
弱引用 | 垃圾回收时 | 非必须的对象缓存 | 上一次垃圾回收结束到下一次垃圾回收开始 |
虚引用 | - | 关联的对象被垃圾收集器回收时候得到一个系统通知 | - |
参考资料:
- JDK11部分源码。
- 《深入理解Java虚拟机-2nd》- 这本书算是国内书籍写得比较良心的一本了,不过有很多小的问题或者笔误之处,需要自行发现和修正。
个人博客
(过年比较懒,很久没发文 e-a-20190215 c-14-d)
深入理解JDK中的Reference原理和源码实现的更多相关文章
- 深入理解JDK中的I/O
深入理解JDK中的I/O 目 录 java内存模型GCHTTP协议事务隔离级并发多线程设计模式清楚redis.memcache并且知道区别mysql分表分库有接口幂等性了解jdk8稍微了解一下特性 j ...
- Dubbo原理和源码解析之“微内核+插件”机制
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
- [Spark内核] 第32课:Spark Worker原理和源码剖析解密:Worker工作流程图、Worker启动Driver源码解密、Worker启动Executor源码解密等
本課主題 Spark Worker 原理 Worker 启动 Driver 源码鉴赏 Worker 启动 Executor 源码鉴赏 Worker 与 Master 的交互关系 [引言部份:你希望读者 ...
- Dubbo原理和源码解析之服务引用
一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...
- Dubbo原理和源码解析之标签解析
一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...
- Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...
- DStream-05 updateStateByKey函数的原理和源码
Demo updateState 可以到达将每次 word count 计算的结果进行累加. object SocketDstream { def main(args: Array[String]): ...
- 我是如何在短期内快速掌握Dubbo的原理和源码的(纯干货)?
写在前面 上周,在[Dubbo系列专题]中更新了两篇文章<冰河开始对Dubbo下手了!>和<俯瞰Dubbo全局,阅读源码前必须掌握这些!!>,收到了很多小伙伴的微信私聊消息,大 ...
- Kubernetes Job Controller 原理和源码分析(一)
概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...
随机推荐
- jquery中获取当前选中行数据的方法
$("table tr").click(function() { var td = $(this).find("td");// 找到td元素 var lo_id ...
- 开发者请注意:Python2 的最后版本将于 4 月发布,但它确实是在 1 月 1 日就寿命终止了!
2020 年 1 月 1 日是 Python2 的寿命终止日,这个日期在两年前经"Python之父" Guido van Rossum 宣布,此后一直成为开发者社区翘首以盼的一天. ...
- docker容器内存占用过高(例如mysql)
简介 该文章适用于配置低,特别是内存低的服务器,在用容器部署服务时有可能会因为容器占用内存过高导致服务挂掉时参考解决(不是运行在容器里的话,也是可以修改mysql的配置文件限制内存占用) 最近用doc ...
- Go Web 编程之 响应
概述 上一篇文章中,我们介绍了请求的结构与处理.本文将详细介绍如何响应客户端的请求.其实在前面几篇文章中,我们已经使用过响应的功能--通过http.ResponseWriter发送字符串给客户端. 但 ...
- 生成TFRecord文件完整代码实例
import os import json def get_annotation_dict(input_folder_path, word2number_dict): label_dict = {} ...
- TensorFlow——dropout和正则化的相关方法
1.dropout dropout是一种常用的手段,用来防止过拟合的,dropout的意思是在训练过程中每次都随机选择一部分节点不要去学习,减少神经元的数量来降低模型的复杂度,同时增加模型的泛化能力. ...
- 艾编程coding老师:深入JVM底层原理与性能调优
1. Java内存模型JMM,内存泄漏及解决方法:2. JVM内存划分:New.Tenured.Perm:3. 垃圾回收算法:Serial算法.并行算法.并发算法:4. JVM性能调优,CPU负载不足 ...
- DP-01背包 (题)
nyoj 325 http://acm.nyist.net/JudgeOnline/problem.php?pid=325 zb的生日 时间限制:3000 ms | 内存限制:65535 KB ...
- Falco 进入 CNCF Incubator 项目 | 云原生生态周报 Vol. 35
作者 | 王思宇.陈洁.敖小剑 业界要闻 Falco 进入 CNCF Incubator 项目 原于 2018 年 8 月进入 sandbox,旨在 Kubernetes 运行时环境下支持配置规则来加 ...
- DOCKER学习_017:Docker-Compose介绍
dockers三驾马车 Docker Machine Docker Swarm Docker Compose 一 Docker Compose介绍 Docker Compose是一个定义和运行多容器应 ...