江义旺:滴滴出行安卓端 finalize time out 的解决方案


桔妹导读:随着安卓 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 堆栈如:java


1java.util.concurrent.TimeoutException: android.content.res.AssetManager$AssetInputStream.finalize() timed out after 15 seconds
2at android.content.res.AssetManager$AssetInputStream.close(AssetManager.java:559)
3at android.content.res.AssetManager$AssetInputStream.finalize(AssetManager.java:592)
4at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:187)
5at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:170)
6at java.lang.Thread.run(Thread.java:841)
android

△ 左滑浏览全貌性能优化


这类 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() 方法中启动了 FinalizerDaemonFinalizerWatchdogDaemon  等关联的守护线程。多线程


1public final class Daemons {
2    ...
3    private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;
4
5    public static void start() {
6        FinalizerDaemon.INSTANCE.start();
7        FinalizerWatchdogDaemon.INSTANCE.start();
8        ...
9    }
10
11    public static void stop() {
12        FinalizerDaemon.INSTANCE.stop();
13        FinalizerWatchdogDaemon.INSTANCE.stop();
14        ...
15    }
16}
架构

△ 左滑浏览全貌并发


Daemons 类主要处理 GC 相关操做,start() 方法调用时启动了 5 个守护线程,其中有 2 个守护线程和这个 BUG 具备直接的关系。app


FinalizerDaemon 析构守护线程


对于重写了成员函数finalize()的类,在对象建立时会新建一个 FinalizerReference 对象,这个对象封装了原对象。当原对象没有被其余对象引用时,这个对象不会被 GC 立刻清除掉,而是被放入 FinalizerReference 的链表中。FinalizerDaemon 线程循环取出链表里面的对象,执行它们的 finalize() 方法,而且清除和对应 FinalizerReference对象引用关系,对应的 FinalizerReference 对象在下次执行 GC 时就会被清理掉。分布式


1private static class FinalizerDaemon extends Daemon {
2    ...
3    @Override public void run() {
4        while (isRunning()) {
5            // Take a reference, blocking until one is ready or the thread should stop
6            try {
7                doFinalize((FinalizerReference<?>) queue.remove());
8            } catch (InterruptedException ignored) {
9            }
10        }
11    }
12
13    @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
14    private void doFinalize(FinalizerReference<?> reference) {
15        ...
16        try {
17            finalizingStartedNanos = System.nanoTime();
18            finalizingObject = object;
19            synchronized (FinalizerWatchdogDaemon.INSTANCE) {
20                FinalizerWatchdogDaemon.INSTANCE.notify();
21            }
22            object.finalize();
23        } catch (Throwable ex) {
24            ...
25        } finally {
26            finalizingObject = null;
27        }
28    }
29}
ide

△ 左滑浏览全貌


FinalizerWatchdogDaemon 析构监护守护线程


析构监护守护线程用来监控 FinalizerDaemon 线程的执行,采用 Watchdog 计时器机制。当 FinalizerDaemon 线程开始执行对象的 finalize() 方法时,FinalizerWatchdogDaemon 线程会启动一个计时器,当计时器时间到了以后,检测 FinalizerDaemon 中是否还有正在执行 finalize() 的对象。检测到有对象存在后就视为 finalize() 方法执行超时,就会产生 TimeoutException 异常。


1private static class FinalizerWatchdogDaemon extends Daemon {
2    ...
3    @Override public void run() {
4        while (isRunning()) {
5            ...
6            boolean finalized = waitForFinalization(object);
7            if (!finalized && !VMRuntime.getRuntime().isDebuggerActive()) {
8                finalizerTimedOut(object);
9                break;
10            }
11        }
12    }
13    ...
14    private boolean waitForFinalization(Object object) {
15        sleepFor(FinalizerDaemon.INSTANCE.finalizingStartedNanos, MAX_FINALIZE_NANOS);
16        return object != FinalizerDaemon.INSTANCE.finalizingObject;//当sleep时间到以后,检测 FinalizerDaemon 线程中当前正在执行 finalize 的对象是否存在,若是存在说明 finalize() 方法超时
17    }
18
19    private static void finalizerTimedOut(Object object) {
20        String message = object.getClass().getName() + ".finalize() timed out after "
21                + (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
22        Exception syntheticException = new TimeoutException(message);
23        syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
24        Thread.UncaughtExceptionHandler h = Thread.getDefaultUncaughtExceptionHandler();
25        ...
26        h.uncaughtException(Thread.currentThread(), syntheticException);
27    }
28}

△ 左滑浏览全貌


由源码能够看出,该 Crash 是在 FinalizerWatchdogDaemon 的线程中建立了一个TimeoutException 传给 Thread 类的 defaultUncaughtExceptionHandler  处理形成的。因为异常中填充了 FinalizerDaemon 的堆栈,之因此堆栈中没有出现和 FinalizerWatchdogDaemon 相关的类。



缘由分析


finalize()致使的  TimeoutException  Crash 很是广泛,不少 APP 都面临着这个问题。使用 finalize() TimeoutException 为关键词在搜索引擎或者 Stack Overflow 上能搜到很是多的反馈和提问,技术网站上对于这个问题的缘由分析大概有两种:


对象 finalize() 方法耗时较长


当 finalize() 方法中有耗时操做时,可能会出现方法执行超时。耗时操做通常有两种状况,一是方法内部确实有比较耗时的操做,好比 IO 操做,线程休眠等。另外有种线程同步耗时的状况也须要注意:有的对象在执行 finalize() 方法时须要线程同步操做,若是长时间拿不到锁,可能会致使超时,如 android.content.res.AssetManager$AssetInputStream 类:


1public final class AssetInputStream extends InputStream {
2    ...
3    public final void close() throws IOException {
4        synchronized (AssetManager.this) {
5            ...
6        }
7    }
8    ...
9    protected void finalize() throws Throwable {
10        close();
11    }
12    ...
13}

△ 左滑浏览全貌


AssetManager 的内部类 AssetInputStream 在执行 finalize() 方法时调用 close() 方法时须要拿到外部类 AssetManager 对象锁, 而在 AssetManager 类中几乎全部的方法运行时都须要拿到一样的锁,若是 AssetManager 连续加载了大量资源或者加载资源是耗时较长,就有可能致使内部类对象 AssetInputStream 在执行finalize() 时长时间拿不到锁而致使方法执行超时。


1public final class AssetManager implements AutoCloseable {
2    ...
3    /*package*/ final CharSequence getResourceText(int ident) {
4        synchronized (this) {
5            ...
6        }
7        return null;
8    }
9    ...
10    public final InputStream open(String fileName, int accessMode) throws IOException {
11        synchronized (this) {
12            ...
13        }
14        throw new FileNotFoundException("Asset file: " + fileName);
15    }
16    ...
17}

△ 左滑浏览全貌


5.0 版本如下机型 GC 过程当中 CPU 休眠致使


有种观点认为系统可能会在执行 
finalize() 方法时进入休眠, 而后被唤醒恢复运行后,会使用如今的时间戳和执行 finalize() 以前的时间戳计算耗时,若是休眠时间比较长,就会出现 TimeoutException

详情请见∞


确实这两个缘由可以致使 finalize() 方法超时,可是从 Crash 的机型分布上看大部分是发生在系统类,另外在 5.0 以上版本也有大量出现,所以咱们认为可能也有其余缘由致使此类问题:


IO 负载太高


许多类的 finalize() 都须要释放 IO 资源,当 APP 打开的文件数目过多,或者在多进程或多线程并发读取磁盘的状况下,随着并发数的增长,磁盘 IO 效率将大大降低,致使 finalize() 方法中的 IO 操做运行缓慢致使超时。


FinalizerDaemon 中线程优先级太低


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  try {
2    Class<?> c = Class.forName(“java.lang.Daemons”);
3    Field maxField = c.getDeclaredField(“MAX_FINALIZE_NANOS”);
4    maxField.setAccessible(true);
5    maxField.set(null, Long.MAX_VALUE);
6catch (Exception e) {
7    ...
8}

△ 左滑浏览全貌


详情请见∞


这种方案思路是有效的,可是这种方法倒是无效的。Daemons 类中 的 MAX_FINALIZE_NANOS 是个  long 型的静态常量,代码中出现的 MAX_FINALIZE_NANOS 字段在编译期就会被编译器替换成常量,所以运行期修改是不起做用的。MAX_FINALIZE_NANOS默认值是 10s,国内厂商经常会修改这个值,通常有 15s,30s,60s,120s,咱们能够推测厂商修改这个值也是为了加大超时的阙值,从而缓解此类 Crash。


· 手动停掉 FinalizerWatchdogDaemon 线程


1    try {
2        Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
3        Method method = clazz.getSuperclass().getDeclaredMethod("stop");
4        method.setAccessible(true);
5        Field field = clazz.getDeclaredField("INSTANCE");
6        field.setAccessible(true);
7        method.invoke(field.get(null));
8    } catch (Throwable e) {
9        e.printStackTrace();
10    }

△ 左滑浏览全貌


详情请见∞


这种方案利用反射 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 逻辑以下:


1public class RuntimeInit {
2    ...
3   private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
4       public void uncaughtException(Thread t, Throwable e) {
5           try {
6                ...
7               // Bring up crash dialog, wait for it to be dismissed 展现APP中止运行对话框
8               ActivityManagerNative.getDefault().handleApplicationCrash(
9                       mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
10           } catch (Throwable t2) {
11                ...
12           } finally {
13               // Try everything to make sure this process goes away.
14               Process.killProcess(Process.myPid()); //退出进程
15               System.exit(10);
16           }
17       }
18   }
19
20    private static final void commonInit() {
21        ...
22        /* set default handler; this applies to all threads in the VM */
23        Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
24        ...
25    }
26}

△ 左滑浏览全貌


从系统默认的 UncaughtExceptionHandler 中能够看出,APP Crash 时弹出的中止运行对话框以及退出进程操做都是在这里处理中处理的,那么只要不让这个代码继续执行就能够阻止 APP 中止运行了。基于这个思路能够将这个方案表示为以下的代码:


1final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
2Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
3    @Override
4    public void uncaughtException(Thread t, Throwable e) {
5        if (t.getName().equals("FinalizerWatchdogDaemon") && e instanceof TimeoutException) {
6             //ignore it
7        } else {
8            defaultUncaughtExceptionHandler.uncaughtException(t, e);
9        }
10    }
11});

△ 左滑浏览全貌


· 可行性


这种方案在 FinalizerWatchdogDaemon 出现 TimeoutException 时主动忽略这个异常,阻断 UncaughtExceptionHandler 链式调用,使系统默认的 UncaughtExceptionHandler 不会被调用,APP 就不会中止运行而继续存活下去。因为这个过程用户无感知,对用户无明显影响,能够最大限度的减小对用户的影响。


· 优势


  • 1. 对系统侵入性小,不中断 FinalizerWatchdogDaemon 的运行。

  • 2.Thread.setDefaultUncaughtExceptionHandler() 方法是公开方法,兼容性比较好,能够适配目前全部 Android 版本。


总结


无论什么样的缓解措施,都是治标不治本,没有从根源上解决。对于这类问题来讲,虽然人为阻止了 Crash,避免了 APP 中止,APP 可以继续运行,可是 finalize() 超时仍是客观存在的,若是 finalize() 一直超时的情况得不到缓解,将会致使 FinalizerDaemon 中 FinalizerReference 队列不断增加,最终出现 OOM 。所以还须要从一点一滴作起,优化代码结构,培养良好的代码习惯,从而完全解决这个问题。固然 BUG 不断,优化不止,在解决问题的路上,缓解止损措施也是很是重要的手段。谁能说能抓老鼠的白猫不是好猫呢?


END

转载请至 / 转载合做入口




江 义 旺  
滴滴 | 业务平台技术
资深软件研发工程师


曾就任于奇虎360,长期从事移动端研发,2018年加入滴滴,专一于安卓移动端性能优化,架构演进,新技术探索,开源项目DroidAssist 做者。



看完江义旺同窗的分享
你有怎样的心得体会?
请在留言区告诉咱们吧



滴滴联合GoCN:第五届 Gopher China 大会来了!

独家揭秘!滴滴云核心技术与创新实践
滴滴开源分布式消息中间件产品DDMQ


本文分享自微信公众号 - 滴滴技术(didi_tech)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索