前言
- Java Reference 类型 是与虚拟机垃圾回收机制密切相关的知识点,同时也是面试重要考点之一。一般认为 Java 有四种 Reference(强引用 & 软引用 & 弱引用 & 虚引用),但是其实还有隐藏的第五种 Reference,你知道是什么吗?
- 在这篇文章里,我将总结 引用类型的用法 & 区别,并基于 ART 虚拟机分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
提示: 本文源码分析基于 Android 9.0 ART 虚拟机。
目录
1. 概述
1.1 什么是引用?
在 Java 中,引用的基本定义是:某一个对象 / 某一块内存的起始地址,这与 C/C++ 中指针的定义是类似的。从 JDK 1.2 开始,Java 扩充了引用的种类,根据引用强度的不同分为四种类型:强引用 & 软引用 & 弱引用 & 虚引用。
1.2 引用的作用
不同引用类型的作用不尽相同,这一点很多文章没有明确指出。软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,而虚引用提供了感知对象垃圾回收的能力。 除了虚引用之外,Object#finalize() 也提供了感知对象被垃圾回收的能力,在 第 5 节 我将分析两者的原理与区别。
引用类型 | Class | 作用 | 对象 GC 时机(不考虑 GC 策略) |
强引用 | 无 | / | GC Root 可达就不会回收 |
软引用 | SoftReference | 灵活控制生存期 | 空闲内存不足以分配新对象时 |
弱引用 | WeakReference | 灵活控制生存期 | 每次GC |
虚引用 | PhantomReference | 感知对象垃圾回收 | 每次GC |
提示: 对象是否被 GC,不仅仅取决于引用类型,还取决于当次 GC 采用的策略。
1.3 对象的访问定位方式
根据引用访问对象,分为 句柄访问 & 直接指针访问 两种方式,你可以看我之前写过的一篇文章:《Java | Object obj = new Object() 占用多少字节?》
2. 引用 & 引用队列
这一节,我们先来分析下引用(Reference)& 引用队列(ReferenceQueue)的源码,以从中梳理出两者基本的依赖关系。
再次提示: 本文源码分析基于 Android 9.0 ART 虚拟机。
2.1 Reference 源码分析
Reference 是抽象类,有四个子类:
- SoftReference(软引用)
- WeakReference(弱引用)
- PhantomReference(虚引用)
- FinalizerReference(@hide)
前三个相信你都见过,第四个 FinalizerReference 是 @hide
隐藏类,我在 第 4 节 再说。首先,我们还是先分析下 Reference 类的源码:
Reference.java
public abstract class Reference<T> { 1、构造器 Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = queue; } 2.1 引用指向的对象 private T referent; 2.2 获取引用指向的对象,如果对象被回收,返回 null public T get() { return getReferent(); } 2.3 清除引用关系 public void clear() { clearReferent(); } 3、关联的引用队列 final ReferenceQueue<? super T> queue; 4、疑问:这两个变量是什么作用呢? Reference queueNext; Reference<?> pendingNext; private final native T getReferent(); native void clearReferent(); ... } 复制代码
这段源码并不复杂,主要关注以下几点:
- 1、创建引用对象的时候可以指定关联的 ReferenceQueue,默认为 null;
- 2、
referent
是引用指向的对象; - 3、
queue
是关联的引用队列 ; - 4、
queueNext & pendingNext
我在 第 2.2 节 讲。
可以看到,获取引用指向的对象和清除引用关系都是调用 native 方法:
static jobject Reference_getReferent(JNIEnv* env, jobject javaThis) { ScopedFastNativeObjectAccess soa(env); ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis); 通过 ReferenceProcessor 获得对象 ObjPtr<mirror::Object> const referent = Runtime::Current()->GetHeap()->GetReferenceProcessor()->GetReferent(soa.Self(), ref); return soa.AddLocalReference<jobject>(referent); } static void Reference_clearReferent(JNIEnv* env, jobject javaThis) { ScopedFastNativeObjectAccess soa(env); ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis); 通过 ReferenceProcessor 清除引用关系 Runtime::Current()->GetHeap()->GetReferenceProcessor()->ClearReferent(ref); } 复制代码
其中的ReferenceProcessor
是 ART 中专门用与处理 Reference 对象的模块,后文我会重新提到。另外,对于 PhantomReference 来说,get()
方法永远返回 null。
PhantomReference.java
public T get() { return null; } 复制代码
2.2 ReferenceQueue 源码分析
引用队列(ReferenceQueue)需要搭配软引用、弱引用和虚引用,源码如下:
ReferenceQueue.java
public class ReferenceQueue<T> { private Reference<? extends T> head = null; private Reference<? extends T> tail = null; public ReferenceQueue() { } 入队 boolean enqueue(Reference<? extends T> reference) { synchronized (lock) { if (enqueueLocked(reference)) { lock.notifyAll(); return true; } return false; } } 入队(内部) private boolean enqueueLocked(Reference<? extends T> r) { .... } 出队 public Reference<? extends T> poll() { ... } } 复制代码
从源码可以看出,ReferenceQueue 是基于单链表的队列,其中方法内部的实现细节我就不贴出来了,不重要。
在这里我们主要关注下面几个方法:
- ReferenceQueue.add(...)
ReferenceQueue.add(...)
是静态方法,源码如下:
public static Reference<?> unenqueued = null; 静态方法:添加一个 Reference 对象 static void add(Reference<?> list) { synchronized (ReferenceQueue.class) { if (unenqueued == null) { 1、如果 unenqueued 为 null,则直接赋值 unenqueued = list; } else { 2.1 找到 unenqueued 的队尾 Reference<?> last = unenqueued; while (last.pendingNext != unenqueued) { last = last.pendingNext; } 2.2 将引用追加到 unenqueued 尾部 last.pendingNext = list; last = list; while (last.pendingNext != list) { last = last.pendingNext; } last.pendingNext = unenqueued; } 3、唤醒等待 ReferenceQueue.class 锁的线程 ReferenceQueue.class.notifyAll(); } } 复制代码
可以看到,这个方法其实就是把参数 Reference 对象追加到unenqueued
尾部。需要注意到,将对象追加到尾部后,还唤醒了等待 ReferenceQueue.class 锁的线程。这个线程在哪里呢?我在 第 3 节 讲。
- ReferenceQueue.enqueuePending(...)
ReferenceQueue.enqueuePending(...)
是静态方法,源码如下:
静态方法:引用入队 public static void enqueuePending(Reference<?> list) { Reference<?> start = list; do { 获取引用关联的引用队列 ReferenceQueue queue = list.queue; if (queue == null) { 1、如果引用没有关联的 ReferenceQueue,跳过 Reference<?> next = list.pendingNext; list.pendingNext = list; list = next; } else { 2、如果引用有关联的 ReferenceQueue synchronized (queue.lock) { 2.1 遍历 pendingNext,如果属于该 queue,则执行入队 do { Reference<?> next = list.pendingNext; list.pendingNext = list; 入队 queue.enqueueLocked(list); list = next; } while (list != start && list.queue == queue); 2.2 唤醒在 queue.lock上等待锁的线程 queue.lock.notifyAll(); } } } while (list != start); } 复制代码
以上源码比较绕,其实这个方法就是 将引用对象添加到关联的引用队列中,随后唤醒了在 queue.lock 上等待锁的线程。
2.3 小结
看到这里,我们先来总结这一节的内容以及遇到的疑问:
- 1、在新建引用对象时,引用与引用队列建立关联,后者是基于单链表的队列;
- 2、静态方法 ReferenceQueue.add(...) 将参数 Reference 对象追加到 unenqueued 尾部,随后唤醒了等待 ReferenceQueue.class 锁的线程;
- 3、静态方法 ReferenceQueue.enqueuePending(...) 将引用对象添加到关联的引用队列中,随后唤醒在 queue.lock 上等待锁的线程。
那么,这些等待的线程在哪里呢?
3. 守护线程
在虚拟机启动时,会启动一些守护线程:
void Runtime::StartDaemonThreads() { 调用 java.lang.Daemons.start() Thread* self = Thread::Current(); JNIEnv* env = self->GetJniEnv(); env->CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons, WellKnownClasses::java_lang_Daemons_start); } 复制代码
public static void start() { 启动四个守护线程 ReferenceQueueDaemon.INSTANCE.start(); FinalizerDaemon.INSTANCE.start(); FinalizerWatchdogDaemon.INSTANCE.start(); HeapTaskDaemon.INSTANCE.start(); } private static abstract class Daemon implements Runnable { private Thread thread; private String name; protected Daemon(String name) { this.name = name; } public synchronized void start() { startInternal(); } public void startInternal() { thread = new Thread(ThreadGroup.systemThreadGroup, this, name); thread.setDaemon(true); thread.start(); } public void run() { runInternal(); } public abstract void runInternal(); protected synchronized boolean isRunning() { return thread != null; } } 复制代码
Daemon 是Runnable 的抽象子类,它的四个实现类分别是 ReferenceQueueDaemon、FinalizerDaemon、FinalizerWatchdogDaemon 和 HeapTaskDaemon,类图如下:
引用自 weread.qq.com/web/reader/… —— 邓凡平 著
3.1 ReferenceQueueDaemon 线程
private static class ReferenceQueueDaemon extends Daemon { private static final ReferenceQueueDaemon INSTANCE = new ReferenceQueueDaemon(); ReferenceQueueDaemon() { super("ReferenceQueueDaemon"); } @Override public void runInternal() { while (isRunning()) { Reference<?> list; 1、同步 synchronized (ReferenceQueue.class) { 2、检查 - 等待 while (ReferenceQueue.unenqueued == null) { ReferenceQueue.class.wait(); } list = ReferenceQueue.unenqueued; ReferenceQueue.unenqueued = null; } 3、将对象加入引用队列 ReferenceQueue.enqueuePending(list); } } } 复制代码
可以看到,ReferenceQueueDaemon 线程的主要作用是轮询判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用上一节讲的 ReferenceQueue.enqueuePending(...) 。
提示: 「检查 - 等待」「设置 - 唤醒」,这是典型的守卫暂停模式。
3.2 FinalizerDaemon 线程
已简化 private static class FinalizerDaemon extends Daemon { private static final FinalizerDaemon INSTANCE = new FinalizerDaemon(); 注意:这个队列是 FinalizerReference 的静态变量 private final ReferenceQueue<Object> queue = FinalizerReference.queue; FinalizerDaemon() { super("FinalizerDaemon"); } @Override public void runInternal() { while (isRunning()) { 1、从引用队列中取出引用 FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll(); 2、执行引用所指向对象 Object#finalize() doFinalize(finalizingReference); } @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION") private void doFinalize(FinalizerReference<?> reference) { 2.1 移除 FinalizerReference 对象 FinalizerReference.remove(reference); 2.2 取出引用所指向的对象 Object object = reference.get(); 2.3 清除引用关系 reference.clear(); 2.4 调用 Object#finalize() object.finalize(); } } 复制代码
可以看到,FinalizerDaemon线程 的主要作用是轮询从引用队列中取出引用,并执行 Object#finalize() 。需要留意到这个队列其实是 FinalizerReference 的静态变量。FinalizerReference 就是 第 2.1 节 提到的 Reference 的子类之一(@hide),我在 第 4 节 再说。
3.3 FinalizerWatchdogDaemon 线程
用于监听 Object#finalize() 的执行耗时,如果执行时间超过MAX_FINALIZE_NANOS
,则会退出虚拟机
private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND; Os.kill(Os.getpid(), OsConstants.SIGQUIT); 复制代码
3.4 小结
看到这里,我们先来总结这一节的内容以及遇到的疑问:
- 1、ReferenceQueueDaemon 守护线程等待 ReferenceQueue.class 的锁,轮询判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用 ReferenceQueue.enqueuePending(...) ;
- 2、FinalizerDaemon 守护线程等待 queue.lock 锁,并轮询从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。
那么,FinalizerReference.queue 中的引用是从哪里来的呢?
4. finalize() 函数执行原理分析
4.1 finalizable 标记位
ClassLinker 在加载类时,用于解析其成员方法的函数 LoadMethod(),会检查方法名是否为 finalize(),是则标记该类为 finalizable。
4.2 新建 FinalizerReference 对象
如果一个类被标记为 finalizable,在新建对象时,ART 虚拟机会调用Heap:AddFinalizerReference(...)
:
void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) { ScopedObjectAccess soa(self); ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object)); jvalue args[1]; args[0].l = arg.get(); 调用 java.lang.ref.FinalizerReference.add(...) InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args); *object = soa.Decode<mirror::Object>(arg.get()); } 复制代码
public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>(); private static FinalizerReference<?> head = null; private FinalizerReference<?> prev; private FinalizerReference<?> next; public static void add(Object referent) { FinalizerReference<?> reference = new FinalizerReference<Object>(referent, queue); synchronized (LIST_LOCK) { 头插法 reference.prev = null; reference.next = head; if (head != null) { head.prev = reference; } head = reference; } } 复制代码
可以看到,每创建一个标记为finalizable 类实例的对象,ART 虚拟机还创建一个指向它的 FinalizerReference 对象,并将 FinalizerReference 对象加入 FinalizerReference 静态成员变量 queue。
4.3 垃圾回收
虚拟机在即将回收对象时,会调用 第 2.2 节 提到的ReferenceQueue.add(...)
:
class ClearedReferenceTask : public HeapTask { ... InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args); ... }; 复制代码
4.4 执行 finalize() 方法
执行 finalize() 方法的源码我们在 第 3.2 节 讲了,要点是:FinalizerDaemon 线程等待 queue.lock 锁,并轮询从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。
4.5 小结
看到这里,我们先来总结这一节的内容:
- 1、重写了 Object#finalize() 的类,在新建对象同时会新建关联的 FinalizerReference;
- 2、在对象即将被 GC 时,会调用 ReferenceQueue.add(...),将引用对象追加到 unenqueued 尾部,并唤醒等待 ReferenceQueue.class 锁的线程;
- 3、ReferenceQueueDaemon 守护线程被唤醒,判断 ReferenceQueue.unenqueued 是否为空,如果不为空则调用 ReferenceQueue.enqueuePending(...),并唤醒等待 queue.lock 锁的线程;
- 4、FinalizerDaemon 守护线程被唤醒,从 FinalizerReference.queue 中取出引用,执行 Object#finalize() 。
5. 感知对象垃圾回收
除了虚引用之外,Object#finalize() 也提供了感知对象被垃圾回收的能力,但是虚引用更加优雅,性能更高。
主要原因是 Object#finalize() 排队在 FinalizeDaemon 守护线程中执行的,由于守护线程的优先级低于其他线程。在 CPU 资源紧张的情况,守护线程竞争到的 CPU 时间片少,这个时候引用对象就会堆积在队列里,增大 OOM 的风险,回收时机也不稳定。
相比之下,使用虚引用的话,可以根据情况使用多个线程来处理。或者直接使用 PhantomReference 的子类 Cleaner 更为简便。
public class Cleaner extends PhantomReference<Object> { ... } 复制代码
6. 总结
- 从 JDK 1.2 开始,Java 扩充了引用的种类,软引用 & 弱引用提供了更加灵活地控制对象生存期的能力,虚引用提供了感知对象垃圾回收的能力;
- 强引用只有当对象没有到 GC Root 的引用链时可回收;软引用不保证每次 GC 都会被回收,只有当空闲内存不足以分配新对象时被回收;弱引用每次 GC 都会被回收;虚引用跟回收时机没有关系,只是提供了一种感知对象垃圾回收的能力;
- FinalizerReference 也是一种引用类型,是隐藏类,用于实现在回收对象之前调用 Object#finalize() 的功能;
- Object#finalize() 也提供了感知对象被垃圾回收的能力,但由于 finalize() 是在守护线程执行的,在 CPU 资源紧张时引用会堆积在引用队列中,增大 OOM 风险,回收时机也不稳定。