出品 | 滴滴技术java
做者 | 江义旺android
前言:随着安卓 APP 规模愈来愈大,代码愈来愈多,各类疑难杂症问题也随之出现。比较常见的一个问题就是 GC finalize() 方法出现 java.util.concurrent.TimeoutException,这类问题难查难解,困扰了不少开发者。那么这类问题是怎么出现的呢?有什么解决办法呢?这篇文章为将探索 finalize() timeout 的缘由和解决方案,分享咱们的踩坑经验,但愿对遇到此类问题的开发者有所帮助。性能优化
在一些大型安卓 APP 中,常常会遇到一个奇怪的 BUG:ava.util.concurrent.TimeoutException
其表现为对象的 finalize() 方法超时,如 android.content.res.AssetManager.finalize() timed out after 10 seconds 。
此前滴滴出行安卓端曾长期受此 BUG 的影响,天天有一些用户会所以遇到 Crash,通过深度分析,最终找到有效解决方案。这篇文章将对这个 BUG 的前因后果以及咱们的解决方案进行分析。
多线程
finalize() TimeoutException
发生在不少类中,典型的 Crash 堆栈如:
架构
这类 Crash 都是发生在 java.lang.Daemons$FinalizerDaemon.doFinalize
方法中,直接缘由是对象的 finalize()
方法执行超时。系统版本从 Android 4.x 版本到 8.1 版本都有分布,低版本分布较多,出错的类有系统的类,也有咱们本身的类。因为该问题在 4.x 版本中最具备表明性,下面咱们将基于 AOSP 4.4 源码进行分析:并发
首先从 Daemons
和 FinalizerDaemon
的由来开始分析,Daemons
开始于 Zygote
进程:Zygote
建立新进程后,经过 ZygoteHooks
类调用了 Daemons
类的 start()
方法,在 start()
方法中启动了 FinalizerDaemon
,FinalizerWatchdogDaemon
等关联的守护线程。函数
Daemons
类主要处理 GC 相关操做,start()
方法调用时启动了 5 个守护线程,其中有 2 个守护线程和这个 BUG 具备直接的关系。源码分析
对于重写了成员函数finalize()
的类,在对象建立时会新建一个 FinalizerReference
对象,这个对象封装了原对象。当原对象没有被其余对象引用时,这个对象不会被 GC 立刻清除掉,而是被放入 FinalizerReference
的链表中。FinalizerDaemon
线程循环取出链表里面的对象,执行它们的 finalize()
方法,而且清除和对应 FinalizerReference
对象引用关系,对应的 FinalizerReference
对象在下次执行 GC 时就会被清理掉。性能
析构监护守护线程用来监控 FinalizerDaemon
线程的执行,采用 Watchdog
计时器机制。当 FinalizerDaemon
线程开始执行对象的 finalize()
方法时,FinalizerWatchdogDaemon
线程会启动一个计时器,当计时器时间到了以后,检测 FinalizerDaemon
中是否还有正在执行 finalize()
的对象。检测到有对象存在后就视为 finalize()
方法执行超时,就会产生 TimeoutException 异常。
优化
由源码能够看出,该 Crash 是在 FinalizerWatchdogDaemon
的线程中建立了一个TimeoutException 传给 Thread
类的 defaultUncaughtExceptionHandler
处理形成的。因为异常中填充了 FinalizerDaemon
的堆栈,之因此堆栈中没有出现和 FinalizerWatchdogDaemon
相关的类。
finalize()
致使的 TimeoutException Crash 很是广泛,不少 APP 都面临着这个问题。使用 finalize() TimeoutException 为关键词在搜索引擎或者 Stack Overflow 上能搜到很是多的反馈和提问,技术网站上对于这个问题的缘由分析大概有两种:
当 finalize()
方法中有耗时操做时,可能会出现方法执行超时。耗时操做通常有两种状况,一是方法内部确实有比较耗时的操做,好比 IO 操做,线程休眠等。另外有种线程同步耗时的状况也须要注意:有的对象在执行 finalize()
方法时须要线程同步操做,若是长时间拿不到锁,可能会致使超时,如 android.content.res.AssetManager$AssetInputStream
类:
AssetManager
的内部类 AssetInputStream
在执行 finalize()
方法时调用 close()
方法时须要拿到外部类 AssetManager
对象锁, 而在 AssetManager
类中几乎全部的方法运行时都须要拿到一样的锁,若是 AssetManager
连续加载了大量资源或者加载资源是耗时较长,就有可能致使内部类对象 AssetInputStream
在执行finalize()
时长时间拿不到锁而致使方法执行超时。
有种观点认为系统可能会在执行 finalize()
方法时进入休眠, 而后被唤醒恢复运行后,会使用如今的时间戳和执行 finalize()
以前的时间戳计算耗时,若是休眠时间比较长,就会出现 TimeoutException。
确实这两个缘由可以致使 finalize()
方法超时,可是从 Crash 的机型分布上看大部分是发生在系统类,另外在 5.0 以上版本也有大量出现,所以咱们认为可能也有其余缘由致使此类问题:
许多类的 finalize()
都须要释放 IO 资源,当 APP 打开的文件数目过多,或者在多进程或多线程并发读取磁盘的状况下,随着并发数的增长,磁盘 IO 效率将大大降低,致使 finalize()
方法中的 IO 操做运行缓慢致使超时。
FinalizerDaemon
中运行的线程是一个守护线程,该线程优先级通常为默认级别 (nice=0
),其余高优先级线程得到了更多的 CPU 时间,在一些极端状况下高优先级线程抢占了大部分 CPU 时间,FinalizerDaemon
线程只能在 CPU 空闲时运行,这种状况也可能会致使超时状况的发生,(从 Android 8.0 版本开始,FinalizerDaemon
中守护线程优先级已经被提升,此类问题已经大幅减小)
当问题出现后,咱们应该找到问题的根本缘由,从根源上去解决。然而对于这个问题来讲却不太容易实现,和其余问题不一样,这类问题缘由比较复杂,有系统缘由,也有 APP 自身的缘由,比较难以定位,也难以系统性解决。
理论上咱们能够作的措施有:
1. 减小对 finalize()
方法的依赖,尽可能不依靠 finalize()
方法释放资源,手动处理资源释放逻辑。
2. 减小 finalizable 对象个数,即减小有 finalize()
方法的对象建立,下降 finalizable 对象 GC 次数。
3.finalize()
方法内尽可能减小耗时以及线程同步时间。
4. 减小高优先级线程的建立和使用,下降高优先级线程的 CPU 使用率。
理想状况下的措施,能够从根本上解决此类问题,但现实状况下却不太容易彻底作到,对一些大型APP来讲更难以完全解决。那么在解决问题的过程当中,有没有别的办法可以缓解或止损呢?总结了技术网站上现有的方案后,能够总结为如下几种:
· 手动修改 finalize() 方法超时时间
理论上咱们能够作的措施有:
1. 减小对 finalize()
方法的依赖,尽可能不依靠 finalize()
方法释放资源,手动处理资源释放逻辑。
2. 减小 finalizable 对象个数,即减小有 finalize()
方法的对象建立,下降 finalizable 对象 GC 次数。
3.finalize()
方法内尽可能减小耗时以及线程同步时间。
4. 减小高优先级线程的建立和使用,下降高优先级线程的 CPU 使用率。
理想状况下的措施,能够从根本上解决此类问题,但现实状况下却不太容易彻底作到,对一些大型APP来讲更难以完全解决。那么在解决问题的过程当中,有没有别的办法可以缓解或止损呢?总结了技术网站上现有的方案后,能够总结为如下几种:
· 手动修改 finalize() 方法超时时间
这种方案思路是有效的,可是这种方法倒是无效的。Daemons
类中 的 MAX_FINALIZE_NANOS
是个 long 型的静态常量,代码中出现的 MAX_FINALIZE_NANOS
字段在编译期就会被编译器替换成常量,所以运行期修改是不起做用的。MAX_FINALIZE_NANOS
默认值是 10s,国内厂商经常会修改这个值,通常有 15s,30s,60s,120s,咱们能够推测厂商修改这个值也是为了加大超时的阙值,从而缓解此类 Crash。
· 手动停掉 FinalizerWatchdogDaemon 线程
这种方案利用反射 FinalizerWatchdogDaemon
的 stop()
方法,以使 FinalizerWatchdogDaemon
计时器功能永远中止。当 finalize()
方法出现超时, FinalizerWatchdogDaemon
由于已经中止而不会抛出异常。这种方案也存在明显的缺点:
1. 在 Android 5.1 版本如下系统中,当 FinalizerDaemon
正在执行对象的 finalize()
方法时,调用 FinalizerWatchdogDaemon
的 stop()
方法,将致使 run()
方法正常逻辑被打断,错误判断为 finalize()
超时,直接抛出 TimeoutException。
2. Android 9.0 版本开始限制 Private API 调用,不能再使用反射调用 Daemons
以及 FinalizerWatchdogDaemon
类方法。
这些方案都是阻止 FinalizerWatchdogDaemon
的正常运行,避免出现 Crash,从原理上仍是具备可行性的:finalize()
方法虽然超时,可是当 CPU 资源充裕时,FinalizerDaemon
线程仍是能够得到充足的 CPU 时间,从而得到了继续运行的机会,最大可能的延长了 APP 的存活时间。可是这些方案或多或少都是有缺陷的,那么有其余更好的办法吗?
What should we do? We just ignore it.
咱们的方案就是忽略这个 Crash,那么怎么可以忽略这个 Crash 呢?首先咱们梳理一下这个 Crash 的出现过程:
1. FinalizerDaemon 执行对象 finalize() 超时。
2. FinalizerWatchdogDaemon 检测到超时后,构造异常交给 Thread 的 defaultUncaughtExceptionHandler 调用 uncaughtException() 方法处理。
3. APP 中止运行。
Thread
类的 defaultUncaughtExceptionHandler
咱们很熟悉了,Java Crash 捕获通常都是经过设置 Thread.setDefaultUncaughtExceptionHandler()
方法设置一个自定义的 UncaughtExceptionHandler
,处理异常后经过链式调用,最后交给系统默认的 UncaughtExceptionHandler
去处理,在 Android 中默认的 UncaughtExceptionHandler
逻辑以下:
从系统默认的 UncaughtExceptionHandler
中能够看出,APP Crash 时弹出的中止运行对话框以及退出进程操做都是在这里处理中处理的,那么只要不让这个代码继续执行就能够阻止 APP 中止运行了。基于这个思路能够将这个方案表示为以下的代码:
这种方案在 FinalizerWatchdogDaemon
出现 TimeoutException 时主动忽略这个异常,阻断 UncaughtExceptionHandler
链式调用,使系统默认的 UncaughtExceptionHandler
不会被调用,APP 就不会中止运行而继续存活下去。因为这个过程用户无感知,对用户无明显影响,能够最大限度的减小对用户的影响。
1. 对系统侵入性小,不中断 FinalizerWatchdogDaemon
的运行。
2.Thread.setDefaultUncaughtExceptionHandler()
方法是公开方法,兼容性比较好,能够适配目前全部 Android 版本。
▍总结
无论什么样的缓解措施,都是治标不治本,没有从根源上解决。对于这类问题来讲,虽然人为阻止了 Crash,避免了 APP 中止,APP 可以继续运行,可是 finalize()
超时仍是客观存在的,若是 finalize()
一直超时的情况得不到缓解,将会致使 FinalizerDaemon
中 FinalizerReference
队列不断增加,最终出现 OOM 。所以还须要从一点一滴作起,优化代码结构,培养良好的代码习惯,从而完全解决这个问题。固然 BUG 不断,优化不止,在解决问题的路上,缓解止损措施也是很是重要的手段。谁能说能抓老鼠的白猫不是好猫呢?
江 义 旺
滴滴 | 业务平台技术
资深软件研发工程师
曾就任于奇虎360,长期从事移动端研发,2018年加入滴滴,专一于安卓移动端性能优化,架构演进,新技术探索,开源项目DroidAssist 做者。
看完江义旺同窗的分享
你有怎样的心得体会?
请在留言区告诉咱们吧