Android内存优化(一)之FinalizerDaemon和FinalizerWatchDog多线程内存泄露案例

前期有一个内存泄露case跟多线程相关,简单记录如下:

问题描述

跑一晚上的内存测试后,会出现很多的内存泄露,泄露trace如下

In *********:2.0.0:2.
* ************.editor.photo.app.PhotoEditor has leaked:
* GC ROOT static java.lang.Daemons$FinalizerDaemon.INSTANCE
* references java.lang.Daemons$FinalizerDaemon.oldFinalizingObject
* references android.view.RenderNode.mOwningView
* references android.view.View.mContext
* leaks *************.editor.photo.app.PhotoEditor instance
* Retaining: 23 MB.
* Reference Key: 62e93630-d8b7-4fda-9522-9e295e16d739
* Device: Xiaomi Xiaomi MI 5s Plus natrium
* Android Version: 7.0 API: 24
* Durations: watch=5082ms, gc=164ms, heap dump=2349ms, analysis=37914ms

从上述trace看,泄露都可达到23MB,已经是非常恐怖的泄露问题

分析过程

初步分析

最初怀疑跟object的finalize执行相关,FinalizerDaemon是用来gc的daemon thread,怀疑dump的时候,这个线程正好引用了被监控对象
具体解释如下:
实现了object的finalize()的类在创建时会新建一个FinalizerReference,这个对象是强引用类型,封装了override finalize()的对象,下面直接叫原对象。原对象没有被其他对象引用时(FinalizeReference除外),执行GC不会马上被清除掉,而是放入一个静态链表中(ReferenceQueue),有一个守护线程专门去维护这个链表,如何维护呢?就是轮到该线程执行时就弹出里面的对象,执行它们的finalize(),对应的FinalizerReference对象在下次执行GC时就会被清理掉。
一个堆的FinalizerReference会组成一条双向链表,垃圾回收器应该会持有链表头(链表头在FinalizerReference中为一个静态成员)。
为什么会泄漏:
直接原因就是守护线程优先级比较低,运行的时间比较少。如果较短时间内创建较多的原对象,就会因为守护线程来不及弹出原对象而使FinalizerReference和原对象都得不到回收。无论怎样调用GC都没有用的,因为只要原对象没有被守护线程弹出执行其finalize()方法,FinalizerReference对象就不会被GC回收。
但此种情况我们不可认为其为泄露,进程中还有一个Daemon线程FinalizerWatchdogDaemon,如果拥有finalize的object超过10s(MIUI改成了60s)没有被回收,则发生crash,但leakcanary的watch时间在5秒左右,也就是说,FinalizerDaemon线程引用被监控对象,在系统中是正常现象~跟Leakcanary提了一个issue,https://github.com/square/leakcanary/issues/856

深度分析

但看了下leakcanary源码,发现FinalizerDaemon是用来gc的daemon thread,怀疑dump的时候,这个线程正好引用了被监控对象这种情况应该是不存在的,解释如下
按照我们的假设,FinalizerDaemon.INSTANCE的finalizingObject成员持有被监控对象的引用,很有可能FinalizerDaemon.INSTANCE作为GC ROOT,泄露路径基本为

* GC ROOT static FinalizerDaemon.INSTANCE

* references java.lang.Daemons$FinalizerDaemon.finalizingObject

* references ********(被监控对象)

但看了下GCTrigger,有一句:
System.runFinalization();

看了下执行流:

System.runFinalization->Runtime.runFinalization()->VMRuntime.runFinalization->FinalizerReference.finalizeAllEnqueued 
// Alloate a new sentinel, this creates a FinalizerReference.
Sentinel sentinel;
// Keep looping until we safely enqueue our sentinel FinalizerReference.
// This is done to prevent races where the GC updates the pendingNext
// before we get the chance.
do {
sentinel = new Sentinel();
} while (!enqueueSentinelReference(sentinel));
sentinel.awaitFinalization(timeout);
// Alloate a new sentinel, this creates a FinalizerReference.
Sentinel sentinel;
// Keep looping until we safely enqueue our sentinel FinalizerReference.
// This is done to prevent races where the GC updates the pendingNext
// before we get the chance.
do {
    sentinel = new Sentinel();
} while (!enqueueSentinelReference(sentinel));
sentinel.awaitFinalization(timeout);

在FinalizerReference中会等待队列的元素执行完finalize方法,这样的话,我们假设的场景在Leakcanary工具运行的过程中是不存在的,于是把提过的issue关闭了,排除不是系统问题,也不是工具问题,而是我们的代码出了问题~
泄露真正原因
从深度分析中可以看出,object的finalize方法的执行不会被工具检测为内存泄露,即使出现泄露,泄露元素也应该是
* references java.lang.Daemons$FinalizerDaemon.finalizingObject,而不是* references java.lang.Daemons$FinalizerDaemon.oldFinalizingObject,慢慢的真正的凶手走入视野~

commit 1423bd1d0ad5aefa4a7b19c975ff5ab3a7ed9eff
Author: yuanhuihui <****@****.com>
Date:   Mon Feb 20 17:49:03 2017 +0800
    adjust finnalizer watchdog

    1. It's not foreground process, no need to throw exception when finalizer exceed 10s.
    2. add finnaliing Object checker, whether occur timeout with different finalizing Object.

    Change-Id: I57083da6ae6bd6c358b88e6933a70e86109be299
    Signed-off-by: ****** 

先说下我对于FinalizerDaemon.oldFinalizingObject引起的内存泄露的理解
1:为什么出现内存泄露
《1》大概流程:
系统的许多工具类实现了object的finalize方法,这些类在创建时会新建FinalizerRefernece,封装override finalize方法的对象,是个强引用关系,GC时会将FinalizerReference放入静态链表ReferenceQueue(双向链表),FinalizerDaemon线程维护此链表,当线程获得执行资源时,从队列中弹出里面的FinalizerReference对象,并执行封装的object的finalize方法,而FinalizerWatchdogDeamon线程会监控 FinalizerDaemon线程的执行object.finalize()方法的快慢问题,如果ReferenceQueue是空的,FinalizerWatchdogDeamon线程会睡去,直到ReferenceQueue不为空,FinalizeDaemon线程会去唤醒FinalizerWatchdogDeamon,告诉它有工作来了
《2》泄露出现的场景
FinalizerReference

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;
}

FinalizerDaemon线程的执行过程是OK的,引用关系清理了之后a就可被GC了,但还有一个关键:FinalizerWatchdog线程的waitForFinalization方法中,会将oldFinalizingObject赋值
// MIUI ADD:
FinalizerDaemon.INSTANCE.oldFinalizingObject = FinalizerDaemon.INSTANCE.finalizingObject;
这是个多线程的问题,此时finalizingObject的值不确定,很可能为null,很可能为指向对象a的引用,如果finalizingObject为null,oldFinalizingObject不会持有任何对象的引用,不会出现内存泄露问题,如果finalizingObject为指向对象a的引用,这句话会导致a的引用计数再+1,即使FinalizerDaemon线程执行完了doFinalize方法,也无法被回收,因为oldFinalizingObject依然持有对象a的引用,此时内存泄露就出现了,但如果下一次赋值oldFinalizingObject = finalizingObject发生了,也会导致a的引用计数-1,此时a就可正常被GC掉,泄露又消失了。
因此出现内存泄露的时间窗口:FinalizerDaemon线程先赋值finalizingObject,然后FinalizerWatchdogDaemon线程的waitForFinalization再执行,而且oldFinalizingObject = finalizingObject为最后一次赋值,最后FinalizerDaemon的doFInalize方法再执行 finalizingObject = null; ,此时就会出现内存泄露
因此此内存泄露是个偶现问题,而且比较隐蔽,也不会像FC,ANR那样很容易被发现,引发的内存泄露很可能会在后面的操作中,又被释放了
2:解决方法
<1>觉引入oldFinalizingObject是为了更好的debug,发现finalize超时问题的细节,但这样带来了内存问题,如果此change只是为了debug用,建议revert掉
<2>如果不能revert,感觉可在FinalizerDaemon线程的run方法中,在让FinalizerWatchdogDaemon线程睡去前,把oldFinalizingObject置为null,这样即使oldFinalizingObject = finalizingObject为最后一次赋值,ReferenceQueue为空时,将oldFinalizingObject置为null,会使内存里的对象引用计数恢复正常,可被正常GC掉

progressCounter.lazySet(++localProgressCounter);
// Slow path; block.
FinalizerWatchdogDaemon.INSTANCE.goToSleep();
finalizingReference = (FinalizerReference<?>)queue.remove();
finalizingObject = finalizingReference.get();
progressCounter.set(++localProgressCounter);
FinalizerWatchdogDaemon.INSTANCE.wakeUp();

问题修复

经好袁老师沟通后,决定将此change revert掉修复此问题~问题解决~

猜你喜欢

转载自blog.csdn.net/longlong2015/article/details/79487520