ReferenceQueue and Rumtime.gc()
前言
内存泄漏对于日常简单的业务开发而言 很常见也不常见
(如果你还是说 Handler
那一套的话,除非你使用 Handler 做 timer
,否则 总有一个时间
会释放所谓的内部类引用的 Activity 等对象)。一般作为业务开发能制造内存泄漏的情景,我遇到的有几种(当然可能存在更多的情况):
- 遇到了 timer 类似场景,有个无限循环的任务引用了一些对象,导致其超出了它正常
生命周期
- 单例类瞎引用,单例生命周期一般都伴随着进程的生命周期
- JNI Global Reference 不释放,或者调用一些 native 层的库,但是其自身存在内存泄漏(这其实跟业务开发已经无关了)
- RxJava 没有 dispose,因为 RxJava 的调用创建很多内部类,不小心就可能会出问题。
- 自己代码逻辑不对,强引用存了一些不该存的对象,这些对象又强引用了带有生命周期的对象,如 Activity 等(一般不会出现,但是业务代码多且乱之后,层层引用,你可能都不知道你自己引用了什么)。
一般出现了内存泄漏时,靠 肉眼看代码
+ 脑中 debug
是基本不可能的,大多是通过一些 工具
或者靠 测试压测(内存不断上涨)
来发现的。
LeakCanary 就是 Android 常用的用于检测 Activity、Fragment、View 级别的内存泄漏的工具,但是也不是万能的。如果 LeakCanary 报出调用链我们往往是可以解决的。但是有时候 LeakCanary 是 检测不到
的(与 Activity 等无关,比如列表页数据和 adapter 的问题),这个时候,只能靠我们 dump 内存,MAT 里一个个看对象数量是否合理了。
LeakCanary 检测内存泄漏的核心原理其实是 ReferenceQueue,所以这篇文章来看看 ReferenceQueue 的一些知识。
ReferenceQueue 的一些原理解析
Reference
面试 八股问题
,你知道 Java 中的几种引用类型吗?
其实,不知道你有没有注意过,SoftReference、WeakReference、PhantomReference 构造函数都可以传入一个叫做 ReferenceQueue
的对象,他们之间的关系是怎样的呢?
先分别来认识一下 Reference 和 ReferenceQueue
Reference 概览
经常会看到类似的说法,Reference 在 JVM 中有四种状态,Active、Pending、Enqueued、Inactive。
一开始我找了半天也没在代码中找到类似字样,后来在 rt.jar 基础类库中的 Reference.java 找到了相应的 注释信息
。然而 Android 中使用的应该是 Platform 的 Reference.java,仔细看会发现两个 java 类 不尽相同
,也没有注释明确说明这四个状态,所以后面说的都是 Platform 里的代码。
Reference 结构
Reference 是个链表。
- queueNext 指向入队后的下一个引用(什么是入队后?见后续的关系图)
- pendingNext 指向即将入队(unenqueued)的下一个引用
- queue 指向构造函数传入的 ReferenceQueue
queueNext 和 pendingNext 均是 Reference 类型,queue 为 ReferenceQueue 类型
具体代码如下:
public abstract class Reference<T> {
final ReferenceQueue<? super T> queue;
Reference queueNext;
Reference<?> pendingNext;
}
ReferenceQueue
ReferenceQueue 顾名思义存储 reference 的一个队列(其实还是链表)。
但是其本身有 两个队列
,一个是入队的队列(每个对象独有),一个是未入队的队列(静态成员,共有)
ReferenceQueue 结构
-
队列一:
unenqueued
。十分重要,代码如下public static Reference<?> unenqueued = null;
因为是静态成员,所以是一个共享变量,多线程操作时需要锁操作,多个 ReferenceQueue 也指向同一个 unenqueued (链表头结点)。
-
队列二:
head
。其实就是每个 ReferenceQueue 各自独有
的链表头结点,存储入队的 Reference。
ReferenceQueue 方法
有两个关键方法 add() 和 enqueuePending(),代码如下:
static void add(Reference<?> list) {
synchronized (ReferenceQueue.class) {
if (unenqueued == null) {
unenqueued = list;
} else {
// Find the last element in unenqueued.
Reference<?> last = unenqueued;
while (last.pendingNext != unenqueued) {
last = last.pendingNext;
}
// Add our list to the end. Update the pendingNext to point back to enqueued.
last.pendingNext = list;
last = list;
while (last.pendingNext != list) {
last = last.pendingNext;
}
last.pendingNext = unenqueued;
}
ReferenceQueue.class.notifyAll();
}
}
public static void enqueuePending(Reference<?> list) {
Reference<?> start = list;
do {
ReferenceQueue queue = list.queue;
if (queue == null) {
Reference<?> next = list.pendingNext;
list.pendingNext = list;
list = next;
} else {
synchronized (queue.lock) {
do {
Reference<?> next = list.pendingNext;
list.pendingNext = list;
queue.enqueueLocked(list);
list = next;
} while (list != start && list.queue == queue);
queue.lock.notifyAll();
}
}
} while (list != start);
}
enqueuePending() 中会再调用 enqueueLocked(),enqueueLocked() 代码就不贴了,核心就是 referece.queueNext = newReference,让新的 reference 入队或者就做链表头结点。
add 方法作用是让 reference 进入 unenqueued 队列或者就做链表头结点。
关系图
为了方便理解,关系图如下
值得注意的几点:
-
不同的 ReferenceQueue 的 unenqueued 链表(头结点)是
同一个
。见图中橘红色
和橘黄色
ReferenceQueue 的 unenqueued 节点均指向灰色Reference(unenqueue 队列头结点)
-
ReferenceQueue.add(reference) 方法,会将 reference 加入 unenqueued 链表中,上一个节点的
pendingNext
指向当前入队的节点。且此时 unenqueued 链表中节点的 queueNext 均为 null(因为还没有入队)。见图中墨绿色指向淡绿色 Reference
部分。 -
ReferenceQueue.enqueuePending(reference) 方法,会将 reference 加入
各自对应
的ReferenceQueue 中(若 head == null,第一个 reference 即为 head),之后的调用就加在链表尾部,上一个节点的queueNext
指向当前入队的节点。见图中灰色、深蓝色、墨绿色 Reference 的 入队过程
。所谓的
各自对应
是因为 unenqueued 链表中的节点的 queue 并不指向同一个 ReferenceQueue(图中,墨绿色
Reference 指向橘黄色
ReferenceQueue,灰色
Reference 指向橘红色
ReferenceQueue),enqueuePending 时会判断当前节点是不是属于这个队列,从而入队。
总结一下就是:
ReferenceQueue.add(reference) 将 reference 加入共有的 unenqueued 链表,ReferenceQueue.enqueuePending(reference) 将 reference 加入 ReferenceQueue 独有的链表中。
ReferenceQueue.add() 和 enqueuePending() 如何联动?
其实这也是 ReferenceQueue 可以用于 检测内存泄漏的原因
和 LeakCanary 原理
。
看过 LeakCanary 源码都知道,LeakCanary 就是使用 KeyedWeakReference
其实对于 Activity 可以简单理解为 WeakReference<Activity>
,构建 WeakReference 时传入了 ReferenceQueue,并且在 Activity 的 onDestroy() 生命周期后查看队列中是否有对应的引用,如果没有还会执行 Runtime.gc(),最终判断是否泄漏。
所以原理也很简单,就是当 JVM(ART) 进行垃圾回收时,最终会触发一些操作,使得 ReferenceQueue 中存在了对应的 Reference,其中的过程又是怎样的呢?真实情况就是这样吗?
下面就看一看 GC 过程
Runtime.gc() 过程
Runtime.gc(),调的是 native gc() 方法,对应 native 方法为 JVM_GC()
// OpenjdkJvm.cc
JNIEXPORT void JVM_GC(void) {
if (art::Runtime::Current()->IsExplicitGcDisabled()) {
LOG(INFO) << "Explicit GC skipped.";
return;
}
art::Runtime::Current()->GetHeap()->CollectGarbage(/* clear_soft_references */ false);
}
CollectGarbage() 方法会调用到 CollectClearedReferences(),部分代码如下:
// heap.cc
collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type, GcCause gc_cause, bool clear_soft_references) {
// 略
collector->Run(gc_cause, clear_soft_references || runtime->IsZygote());
IncrementFreedEver();
RequestTrim(self);
// Collect cleared references.
SelfDeletingTask* clear = reference_processor_->CollectClearedReferences(self);
// Grow the heap so that we know when to perform the next GC.
GrowForUtilization(collector, bytes_allocated_before_gc);
LogGC(gc_cause, collector);
FinishGC(self, gc_type);
// Actually enqueue all cleared references. Do this after the GC has officially finished since
// otherwise we can deadlock.
clear->Run(self);
clear->Finalize();
// Inform DDMS that a GC completed.
Dbg::GcDidFinish();
// 略
}
CollectClearedReferences() 其实构建了一个 Task,ClearedReferenceTask,代码如下:
SelfDeletingTask* ReferenceProcessor::CollectClearedReferences(Thread* self) {
Locks::mutator_lock_->AssertNotHeld(self);
// By default we don't actually need to do anything. Just return this no-op task to avoid having
// to put in ifs.
std::unique_ptr<SelfDeletingTask> result(new FunctionTask([](Thread*) {}));
// When a runtime isn't started there are no reference queues to care about so ignore.
if (!cleared_references_.IsEmpty()) {
if (LIKELY(Runtime::Current()->IsStarted())) {
jobject cleared_references;
{
ReaderMutexLock mu(self, *Locks::mutator_lock_);
cleared_references = self->GetJniEnv()->GetVm()->AddGlobalRef(
self, cleared_references_.GetList());
}
if (kAsyncReferenceQueueAdd) {
// TODO: This can cause RunFinalization to terminate before newly freed objects are
// finalized since they may not be enqueued by the time RunFinalization starts.
Runtime::Current()->GetHeap()->GetTaskProcessor()->AddTask(
self, new ClearedReferenceTask(cleared_references));
} else {
result.reset(new ClearedReferenceTask(cleared_references));
}
}
cleared_references_.Clear();
}
return result.release();
}
最终执行的是 ClearedReferenceTask 的 Run 方法,代码如下:
class ClearedReferenceTask : public HeapTask {
public:
explicit ClearedReferenceTask(jobject cleared_references)
: HeapTask(NanoTime()), cleared_references_(cleared_references) {
}
void Run(Thread* thread) override {
ScopedObjectAccess soa(thread);
jvalue args[1];
args[0].l = cleared_references_;
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args);
soa.Env()->DeleteGlobalRef(cleared_references_);
}
private:
const jobject cleared_references_;
};
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args);
这行代码一看就很熟悉,ReferenceQueue_add 方法。
所以,找到了初步关系就是:
ART 在执行垃圾回收时,如果存在需要回收的对象,会调用其所对应的 ReferenceQueue.add() 方法。
ReferenceQueue.add() 方法中会执行 ReferenceQueue.class.notifyAll()
notify 肯定是 唤醒一个正在等待的线程
,那是谁呢?
这部分的逻辑其实在 Daemons.java 中,Daemons.java 会启动四个线程,其中有两个很熟悉,FinalizerDaemon
和 ReferenceQueueDaemon
。
其中 FinalizerDaemon 会执行重写了 finalize 函数的对象的 finalize() 方法(这个过程是如何实现的可以网上搜一搜)
ReferenceQueueDaemon 是今天的重点,它是引用队列的守护线程。代码如下:
// Daemons.java
private static class ReferenceQueueDaemon extends Daemon {
private static final ReferenceQueueDaemon INSTANCE = new ReferenceQueueDaemon();
ReferenceQueueDaemon() {
super("ReferenceQueueDaemon");
}
@Override public void runInternal() {
while (isRunning()) {
Reference<?> list;
try {
synchronized (ReferenceQueue.class) {
while (ReferenceQueue.unenqueued == null) {
ReferenceQueue.class.wait();
}
list = ReferenceQueue.unenqueued;
ReferenceQueue.unenqueued = null;
}
} catch (InterruptedException e) {
continue;
} catch (OutOfMemoryError e) {
continue;
}
ReferenceQueue.enqueuePending(list);
}
}
}
还记得 add() 最终执行的 ReferenceQueue.class.notifyAll() 吗?这里 ReferenceQueueDaemon 正在等待被唤醒,之后会执行 ReferenceQueue.enqueuePending(list),就此打通所有流程。
ReferenceQueueDaemon 又是怎么启动的?其实是 Daemons.start()
启动的(具体过程见流程梳理)
流程梳理
第一步
JVM/ART 启动时,会
启动四个 Daemon 线程
,其中包括了 ReferenceQueueDaemon,并且其会因为 unenqueued == null 而 wait() 被挂起。
具体启动流程可以参见 Android ART 虚拟机启动流程,大致为 init 进程启动 zygote 进程,app_main.cpp 中会调用 Runtime::Start()(这个函数比较长),从而执行到 StartDaemonThreads(),其代码如下:
void Runtime::StartDaemonThreads() {
ScopedTrace trace(__FUNCTION__);
VLOG(startup) << "Runtime::StartDaemonThreads entering";
Thread* self = Thread::Current();
// Must be in the kNative state for calling native methods.
CHECK_EQ(self->GetState(), kNative);
JNIEnv* env = self->GetJniEnv();
env->CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons,
WellKnownClasses::java_lang_Daemons_start);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
LOG(FATAL) << "Error starting java.lang.Daemons";
}
VLOG(startup) << "Runtime::StartDaemonThreads exiting";
}
可以看到 CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons, WellKnownClasses::java_lang_Daemons_start) 被调用,从而调用了 Daemons.start(),启动了四个 Daemon 线程。
第二步
JVM/ART GC 时,会调用 ReferenceQueue 的 ReferenceQueue.add(Reference),最终
notifyAll 唤醒 ReferenceQueueDaemon 线程
,执行 ReferenceQueue.enqueuePending(list) 方法
具体如何调用到 add 方法,见上面的分析。
第三步
ReferenceQueue.enqueuePending(list) 会执行到 ReferenceQueue.enqueueLocked() 方法,从而使得这个即将被回收的对象的 Reference 对象进入其所在的 ReferenceQueue 中(入队后 Reference 已经不再持有原对象)
通过以上三步,我们知道:
如果一个对象被回收了,比如是 WeakReference 方式,且创建时传入了 ReferenceQueue,那么此 WeakReference 会最终进入到其对应的 ReferenceQueue 中。
LeakCanary 最本质的原理就是这样,检测 ReferenceQueue 有无此 Reference,有则没有泄露,应该还需要自行 remove。没有的话主动调用 Rumtime.gc() 之后再来观察队列,依然没有则判定存在了内存泄漏。
总结收获
整个过程中仍然有许多细节没有深究,但是对于 引用类型
了解更加深刻了。
阅读源码的过程中,也顺便了解了 FinalizerDaemon 线程,了解了 finalize() 的流程,收获了一些 "平时无用的知识"
。