0%

Android-记一次线上OOM问题处理

概述

最近线上监控发现 OOM 涨幅较大,因此去尝试定位和修复这个问题,在修复了一些内存泄漏和大对象占用问题后, OOM 依旧未达到正常标准,在这些新上报的 hprof 文件中,发现几乎所有 case 中都有个叫 FinalizerReference 的对象,数量巨多,内存占用高居首位,因此判断它就是引起本次 OOM 上涨的罪魁祸首。

ReferenceQueue

首先前置了解下 ReferenceQueue 引用队列是个啥,简言之就是用来存放 Reference 对象的队列,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中。即:

1
2
3
val queue = ReferenceQueue<Bean>()
val bean = Bean()
val reference = SoftReference(bean, queue)

上述代码中创建了一个 bean 强引用,以及一个 reference 软引用,当 bean 被回收时,该软引用 reference 对象会被放到 queue 队列中,后续该 reference 对象需要开发者自行处理(如从队列中 poll 等)。

Leakcanary 检测内存泄漏的原理就是应用的 ReferenceQueue 引用队列,以 Activity 为例:

  1. 监听 Activity 的生命周期。
  2. 在 onDestory 的时候,通过 ReferenceQueue 来创建对应 Actitity 的 Refrence 引用对象。
  3. 一段时间后,从 ReferenceQueue 中读取,如果有这个 Actitity 的 Refrence,那么说明这个 Activity 的 Refrence 已经被回收;如果 ReferenceQueue 没有这个 Actitity 的 Refrence, 那就说明出现了内存泄漏。

关于 Reference 引用,网上一搜很多文章,大家估计比较清楚了,就不再赘述。以前做过一些相关的笔记,有兴趣可以看看 JVM垃圾回收-引用

FinalizerReference

首先介绍一下 Finalizer 对象,它指的是在其 Java 类中复写了 finalize() 方法,且该方法非空的对象,有的地方称呼这种类叫 f 类,我们也这样叫它。在类加载过程中会把加载的 Java 类标记为是否 f 类。

FinalizerReference概述

现在开始看看 FinalizerReference 是个啥玩意,知己知彼,才能知道怎么去修它。这个 FinalizerReference 是用来协助 FinalizerDaemon 线程处理对象的 finalize() 工作的,一次性出现了三个名词,我们先从 FinalizerReference 看起。

看看它的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// java/lang/ref/FinalizerReference.java
public final class FinalizerReference<T> extends Reference<T> {
// This queue contains those objects eligible for finalization.
public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();

// This list contains a FinalizerReference for every finalizable object in the heap.
// Objects in this list may or may not be eligible for finalization yet.
private static FinalizerReference<?> head = null;

// The links used to construct the list.
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;
}
}

public static void remove(FinalizerReference<?> reference) {
synchronized (LIST_LOCK) {
FinalizerReference<?> next = reference.next;
FinalizerReference<?> prev = reference.prev;
reference.next = null;
reference.prev = null;
if (prev != null) {
prev.next = next;
} else {
head = next;
}
if (next != null) {
next.prev = prev;
}
}
}
}

FinalizerReference 中有两个静态方法: add 和 remove 方法。它创建了一个 FinalizerReference 类型的链表(表头用静态变量 FinalizerReference.head 表示),其中每个 FinalizerReference 对象都使用 ReferenceQueue 类型的静态变量 queue 来创建,这样当对应的对象 Object referent 被回收后,该 FinalizerReference 会被放入 ReferenceQueue 中。

FinalizerReference.add

上述 FinalizerReference.add 方法是虚拟机调用的,当创建对象的时候,如果发现该类是 f 类,就会调用 FinalizerReference.add 方法创建 FinalizerRefence 对象,并将其加入到 head 链表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// art/runtime/mirror/class-alloc-inl.h
template<bool kIsInstrumented, Class::AddFinalizer kAddFinalizer, bool kCheckAddFinalizer>
inline ObjPtr<Object> Class::Alloc(Thread* self, gc::AllocatorType allocator_type) {
CheckObjectAlloc();
gc::Heap* heap = Runtime::Current()->GetHeap();
bool add_finalizer;
switch (kAddFinalizer) {
case Class::AddFinalizer::kUseClassTag:
// 这里判断是不是 f 类
add_finalizer = IsFinalizable();
break;
case Class::AddFinalizer::kNoAddFinalizer:
add_finalizer = false;
DCHECK_IMPLIES(kCheckAddFinalizer, !IsFinalizable());
break;
}
// Note that the `this` pointer may be invalidated after the allocation.
ObjPtr<Object> obj = heap->AllocObjectWithAllocator<kIsInstrumented, /*kCheckLargeObject=*/ false>(self, this, this->object_size_, allocator_type, VoidFunctor());
if (add_finalizer && LIKELY(obj != nullptr)) {
// 如果是 f 类,则调用 AddFinalizerReference 方法
heap->AddFinalizerReference(self, &obj);
// ...
}
return obj;
}

// art/runtime/gc/heap.cc
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();
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
// Restore object in case it gets moved.
*object = soa.Decode<mirror::Object>(arg.get());
}

上面的 AddFinalizerReference 方法会调用到 Java 层的 FinalizerReference.add() 静态方法,就完成了 add 的操作。

FinalizerReference.remove

我们前面提到,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中,所以当 f 类的对象发生 gc 时,会将其对应的 FinalizerReference 对象加入到 FinalizerReference.queue 队列里。而 remove 的时机与 FinalizerDaemon 守护线程有关,我们看看这个线程的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// libcore/libart/src/main/java/java/lang/Daemons.java
// private static abstract class Daemon implements Runnable
private static class FinalizerDaemon extends Daemon {
private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
private final ReferenceQueue<Object> queue = FinalizerReference.queue;

@Override public void runInternal() {
// ...
while (isRunning()) {
try {
// Use non-blocking poll to avoid FinalizerWatchdogDaemon communication when busy.
FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
// ...
doFinalize(finalizingReference);
} catch (InterruptedException ignored) {
} catch (OutOfMemoryError ignored) {
}
}
}
}

在 FinalizerDaemon.runInternal 方法中会通过 FinalizerReference.queue(ReferenceQueue引用队列) 的 poll/remove 方法拿到 queue 中的 Reference 引用,接着执行 doFinalize() 方法,该方法会调用 Finalizer 对象的 finalize() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void doFinalize(FinalizerReference<?> reference) {
FinalizerReference.remove(reference);
Object object = reference.get();
reference.clear();
try {
object.finalize();
} catch (Throwable ex) {
// The RI silently swallows these, but Android has always logged.
System.logE("Uncaught exception thrown by finalizer", ex);
} finally {
// Done finalizing, stop holding the object as live.
finalizingObject = null;
}
}

首先将获取到的 FinalizerReference 对象从 FinalizerReference.head 链表中移除,接着通过 reference.get() 方法得到 Java 对象,并执行其 finalize() 方法。

小结

FinalizerReference 的主要作用是协助 FinalizerDaemon 守护线程来执行 Finalizer 对象的 finalize() 方法

  • 在 f 类创建对象时,调用 FinalizerReference.add() 方法创建一个 FinalizerReference 对象(使用 ReferenceQueue 队列)并加入到 head 链表里。
  • 在 f 类的对象被回收后,其对应的 FinalizerReference 对象会被加入到 ReferenceQueue 队列(FinalizerReference.queue)里。
  • FinalizerDaemon 守护线程从 FinalizerReference.queue 队列中取出 FinalizerReference 对象,并执行其对应 f 类对象的 finalize() 方法。

ReferenceQueueDaemon

上面看完了 FinalizerDaemon 守护线程,这里再看看 ReferenceQueueDaemon 守护线程,上面说到在创建引用对象 Reference 的时候可以关联一个 ReferenceQueue 队列,当被引用对象被 gc 回收时,该 reference 对象就会被加入到其创建时关联的队列去,这个加入队列的操作就是由 ReferenceQueueDaemon 守护线程完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static class ReferenceQueueDaemon extends Daemon {
@Override public void runInternal() {
while (isRunning()) {
Reference<?> list;
try {
synchronized (ReferenceQueue.class) {
if (ReferenceQueue.unenqueued == null) {
progressCounter.incrementAndGet();
do {
ReferenceQueue.class.wait();
} while (ReferenceQueue.unenqueued == null);
progressCounter.incrementAndGet();
}
list = ReferenceQueue.unenqueued;
ReferenceQueue.unenqueued = null;
}
} catch (InterruptedException e) {
continue;
} catch (OutOfMemoryError e) {
continue;
}
ReferenceQueue.enqueuePending(list, progressCounter);
}
}
}

具体源码感兴趣的话可以自己去看看,这里就不贴太多源码了。

FinalizerWatchdogDaemon

这里再补充一下 FinalizerWatchdogDaemon 守护线程,它跟 FinalizerDaemon 以及 ReferenceQueueDaemon 线程是一起 start 的。我们再看一下上面 FinalizerDaemon 和 ReferenceQueueDaemon 线程的 runInternal() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ReferenceQueueDaemon
@Override public void runInternal() {
FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
while (isRunning()) {
Reference<?> list;
try {
synchronized (ReferenceQueue.class) {
if (ReferenceQueue.unenqueued == null) {
progressCounter.incrementAndGet();
FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
do {
ReferenceQueue.class.wait();
} while (ReferenceQueue.unenqueued == null);
progressCounter.incrementAndGet();
FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
}
list = ReferenceQueue.unenqueued;
ReferenceQueue.unenqueued = null;
}
} catch (InterruptedException e) {
continue;
} catch (OutOfMemoryError e) {
continue;
}
ReferenceQueue.enqueuePending(list, progressCounter);
FinalizerWatchdogDaemon.INSTANCE.resetTimeouts();
}
}

// FinalizerDaemon
@Override public void runInternal() {
FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
while (isRunning()) {
try {
FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
if (finalizingReference != null) {
finalizingObject = finalizingReference.get();
} else {
finalizingObject = null;
FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
finalizingReference = (FinalizerReference<?>)queue.remove();
finalizingObject = finalizingReference.get();
FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
}
doFinalize(finalizingReference);
} catch (InterruptedException ignored) {
} catch (OutOfMemoryError ignored) {
}
}
}

上面的 monitoringNotNeeded 方法会休眠线程,停止 timeout 计时,而 monitoringNotNeeded 会唤醒 FinalizerWatchdogDaemon 守护线程。看看 FinalizerWatchdogDaemon 的源码,我加了一些注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private static class FinalizerWatchdogDaemon extends Daemon {
// Single bit values to identify daemon to be watched.
// 监控的两种类型
static final int FINALIZER_DAEMON = 1;
static final int RQ_DAEMON = 2;

@Override public void runInternal() {
while (isRunning()) {
// sleepUntilNeeded 内部会调用 wait() 方法,不贴了
if (!sleepUntilNeeded()) {
continue;
}
final TimeoutException exception = waitForProgress();
if (exception != null && !VMDebug.isDebuggerConnected()) {
// 抛出异常 Thread.currentThread().dispatchUncaughtException(exception)
timedOut(exception);
break;
}
}
}

// 去掉 whichDaemon 的监控,进入 wait
private synchronized void monitoringNotNeeded(int whichDaemon) {
activeWatchees &= ~whichDaemon;
}

// 添加 whichDaemon 的监控, notify 线程
private synchronized void monitoringNeeded(int whichDaemon) {
int oldWatchees = activeWatchees;
activeWatchees |= whichDaemon;
if (oldWatchees == 0) {
// 调用 notify 唤醒
notify();
}
}

// 判断是否添加了指定 whichDaemon 类型的监控
private synchronized boolean isActive(int whichDaemon) {
return (activeWatchees & whichDaemon) != 0;
}

// 调用 isActive 判断开启监控的类型,通过 Thread.sleep() 方法休眠指定时间
// 休眠结束后返回 TimeoutException 异常,若途中监控的逻辑执行完成则表示未超时,返回 null
// 等待时间: VMRuntime.getFinalizerTimeoutMs
// If the FinalizerDaemon took essentially the whole time processing a single reference,
// or the ReferenceQueueDaemon failed to make visible progress during that time, return an exception.
private TimeoutException waitForProgress() {
finalizerTimeoutNs = NANOS_PER_MILLI * VMRuntime.getRuntime().getFinalizerTimeoutMs();
// ...
}
}

上面一些细节代码就不贴出了, FinalizerWatchdogDaemon 主要用来监控两种类型的执行时长: FINALIZER_DAEMON 和 RQ_DAEMON。当执行超时时就会抛出 TimeOutException 异常,因此不要在 finalize() 方法中做耗时操作。

这个超时时间在 AOSP 中定义为 10s, 国内厂商可能会修改这个值, 据说有的改为了 60 s, 有兴趣的同学可以自己解包看看:

1
RUNTIME_OPTIONS_KEY (unsigned int,        FinalizerTimeoutMs,             10000u)

OOM排查

通过线上 hprof 文件排除了大对象和内存泄漏的可能,接着在 hprof 中发现存在大量的 X(用 X 表示业务上的某个对象) 对象堆积,这些对象对应的 Java 类是一个与 Native 层有关的类,其重写了 finalize() 方法,线下无法复现堆积大量 X 对象的路径。

可能有某个业务场景的代码逻辑使用不当造成了 X 对象的疯狂创建,导致 FinalizerDaemon 线程来不及回收

由于这个 X 对象的创建场景比较多,因此无法通过代码 review 来定位到有问题的创建代码,采取了一些监控手段后依旧无果。

用治标不治本的方式,显式调用系统 gc 和 runFinalization 方法

在定位不到具体的问题场景后,因为猜测是创建了大量的 X 对象导致 FinalizerDaemon 线程回收不及时,因此分别在主线程和子线程中显式调用系统 gc 和 runFinalization 方法来手动回收 X 对象。

结果:子线程调用无效果, OOM 未下降;主线程调用发生了 ANR。

查看 ANR 堆栈后,发现 ANR 的位置都在另外一个 f 对象的 finalize() 方法中调用到的一行 Native 代码,因此问题就明了了,是因为在某个 finalize() 方法中调用到的这行代码卡死了(逻辑问题导致死锁),因此阻塞了 FinalizerDaemon 线程的执行,进而导致对象堆积。另外本应发生的 TimeoutException 异常被我们的异常处理框架捕获了,因此未曾暴露出来。

总结

Java 中并没有析构函数, finalize() 实现了类似析构函数的概念,可以在对象被回收前做一些回收性的操作,在 JNI 开发中可能被用来进行 Native 内存的释放。关于 f 类使用不当会造成的影响:

  • FinalizerDaemon 守护线程的优先级不高,在 CPU 紧张的情况下调度可能会被影响,所以可能无法及时回收 f 类对象。
  • finalize 对象由于 finalize() 的引用,它变成了一个强引用,即使没有其他显式的强引用了,它也还是无法立即被回收。
  • 因此 f 对象至少需要 2 次 gc 才可能被回收,第一次 gc 时会将 f 对象加入到 ReferenceQueue 中,而等到 finalize() 方法执行完毕后的下一次 gc 里才有可能回收它。由于守护线程的优先级低,这期间可能已经发生过多次 gc 了,长时间未回收的话又可能会导致 f 类在资源紧张时进入到老年代,从而引起老年代的 gc 甚至是 full gc。

因此尽量不要重载 finalize() 方法,而是通过一些逻辑上的接口去释放内存,如果一定要重载它的话,也不要让一些需要频繁创建的对象或者大对象去通过 finalize() 释放,不然总有一天会出现问题的。

与此相关的守护线程有四个,感兴趣的同学可以深入看看相关源码:

1
2
3
4
5
6
7
// libcore/libart/src/main/java/java/lang/Daemons.java
private static final Daemon[] DAEMONS = new Daemon[] {
HeapTaskDaemon.INSTANCE,
ReferenceQueueDaemon.INSTANCE,
FinalizerDaemon.INSTANCE,
FinalizerWatchdogDaemon.INSTANCE,
};