目录

ReferenceQueue and Rumtime.gc()

前言

内存泄漏对于日常简单的业务开发而言 很常见也不常见(如果你还是说 Handler 那一套的话,除非你使用 Handler 做 timer,否则 总有一个时间 会释放所谓的内部类引用的 Activity 等对象)。一般作为业务开发能制造内存泄漏的情景,我遇到的有几种(当然可能存在更多的情况):

  1. 遇到了 timer 类似场景,有个无限循环的任务引用了一些对象,导致其超出了它正常 生命周期
  2. 单例类瞎引用,单例生命周期一般都伴随着进程的生命周期
  3. JNI Global Reference 不释放,或者调用一些 native 层的库,但是其自身存在内存泄漏(这其实跟业务开发已经无关了)
  4. RxJava 没有 dispose,因为 RxJava 的调用创建很多内部类,不小心就可能会出问题。
  5. 自己代码逻辑不对,强引用存了一些不该存的对象,这些对象又强引用了带有生命周期的对象,如 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 是个链表。

  1. queueNext 指向入队后的下一个引用(什么是入队后?见后续的关系图)
  2. pendingNext 指向即将入队(unenqueued)的下一个引用
  3. 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 结构

  1. 队列一: unenqueued。十分重要,代码如下

     public static Reference<?> unenqueued = null;
    

    因为是静态成员,所以是一个共享变量,多线程操作时需要锁操作,多个 ReferenceQueue 也指向同一个 unenqueued (链表头结点)。

  2. 队列二: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 队列或者就做链表头结点。

关系图

为了方便理解,关系图如下

/img/in-post/referenceq.png

值得注意的几点:

  1. 不同的 ReferenceQueue 的 unenqueued 链表(头结点)是 同一个。见图中 橘红色橘黄色 ReferenceQueue 的 unenqueued 节点 均指向灰色Reference(unenqueue 队列头结点)

  2. ReferenceQueue.add(reference) 方法,会将 reference 加入 unenqueued 链表中,上一个节点的 pendingNext 指向当前入队的节点。且此时 unenqueued 链表中节点的 queueNext 均为 null(因为还没有入队)。见图中 墨绿色指向淡绿色 Reference 部分。

  3. 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 会启动四个线程,其中有两个很熟悉,FinalizerDaemonReferenceQueueDaemon

其中 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() 的流程,收获了一些 "平时无用的知识"