【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

滴滴出行安卓端 finalize time out 的解决方案

  • 2019-09-16
  • 本文字数:7167 字

    阅读完需:约 24 分钟

滴滴出行安卓端 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 堆栈如:


1java.util.concurrent.TimeoutException: android.content.res.AssetManager$AssetInputStream.finalize() timed out after 15 seconds2at 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)
复制代码


这类 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 等关联的守护线程。


 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    }1011    public static void stop() {12        FinalizerDaemon.INSTANCE.stop();13        FinalizerWatchdogDaemon.INSTANCE.stop();14        ...15    }16}
复制代码


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

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    }1213    @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}
复制代码

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    }1819    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 自身的原因,比较难以定位,也难以系统性解决。

理想措施

理论上我们可以做的措施有:


  • 减少对 finalize() 方法的依赖,尽量不依靠 finalize() 方法释放资源,手动处理资源释放逻辑。

  • 减少 finalizable 对象个数,即减少有 finalize() 方法的对象创建,降低 finalizable 对象 GC 次数。

  • 3.finalize() 方法内尽量减少耗时以及线程同步时间。

  • 减少高优先级线程的创建和使用,降低高优先级线程的 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);6} catch (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 因为已经停止而不会抛出异常。这种方案也存在明显的缺点:


  • 在 Android 5.1 版本以下系统中,当 FinalizerDaemon 正在执行对象的 finalize() 方法时,调用 FinalizerWatchdogDaemon 的 stop() 方法,将导致 run() 方法正常逻辑被打断,错误判断为 finalize() 超时,直接抛出 TimeoutException。

  • 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 的出现过程:


  • FinalizerDaemon 执行对象 finalize() 超时。

  • FinalizerWatchdogDaemon 检测到超时后,构造异常交给 Thread 的 defaultUncaughtExceptionHandler 调用 uncaughtException() 方法处理。

  • 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   }1920    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 就不会停止运行而继续存活下去。由于这个过程用户无感知,对用户无明显影响,可以最大限度的减少对用户的影响。


  • 优点

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

  • 2.Thread.setDefaultUncaughtExceptionHandler() 方法是公开方法,兼容性比较好,可以适配目前所有 Android 版本。

总结

不管什么样的缓解措施,都是治标不治本,没有从根源上解决。对于这类问题来说,虽然人为阻止了 Crash,避免了 APP 停止,APP 能够继续运行,但是 finalize() 超时还是客观存在的,如果 finalize() 一直超时的状况得不到缓解,将会导致 FinalizerDaemon 中 FinalizerReference 队列不断增长,最终出现 OOM 。因此还需要从一点一滴做起,优化代码结构,培养良好的代码习惯,从而彻底解决这个问题。当然 BUG 不断,优化不止,在解决问题的路上,缓解止损措施也是非常重要的手段。谁能说能抓老鼠的白猫不是好猫呢?


本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接:


https://mp.weixin.qq.com/s/uFcFYO2GtWWiblotem2bGg


2019-09-16 22:133273

评论 1 条评论

发布
用户头像
InfoQ上真不适合发这样的干货,都没啥人看,这里就是吹B的地方。
2020-04-16 15:47
回复
没有更多了
发现更多内容

华为云弹性云服务器ECS使用【华为云至简致远】

IT资讯搬运工

弹性云服务器ECS

面试官:Redis 大 key 要如何处理?

Java永远的神

Java 数据库 redis 程序员 面试

手把手教你设计一个全局异常处理器

了不起的程序猿

java程序员 异常处理 java 编程 spring-boot

零基础培训学习大数据课程

小谷哥

湖北钠斯网络数字藏品交易系统

开源直播系统源码

NFT 数字藏品

Java面试项目推荐,15个项目吃透两个offer拿到手软

冉然学Java

offer java; 技术栈 MAll java项目实战分享

开源一夏 | 自己画一块ESP32-C3 的开发板(PCB到手)

矜辰所致

开源 硬件设计 8月月更 ESP32-C3

兆骑科创赛事服务平台对接,海内外高层次人才引进

兆骑科创凤阁

什么是低代码开发?大家都真的看好低代码开发吗?

优秀

低代码开发

优雅地实时检测和更新 Web 应用

领创集团Advance Intelligence Group

Web Web应用

一文读懂字节跳动“埋点验证平台”

字节跳动数据平台

字节跳动 数据治理 埋点治理 数据研发 埋点验证

JavaScript 里三个点 ...,可不是省略号啊···

华为云开发者联盟

JavaScript 前端 运算符 函数

开源一夏 | Spring事务传播机制

六月的雨在InfoQ

开源 Spring事务 8月月更

客户案例 | 提高银行信用卡客户贡献率

易观分析

金融 银行 分析 客户

跟我一起了解云耀云服务器HECS【华为云至简致远】

IT资讯搬运工

云服务器

基于微信小程序的幼儿园招生报名系统开发笔记

CC同学

国产堡垒机品牌哪家好?功能有哪些?咨询电话多少?

行云管家

运维 堡垒机 运维审计 国产堡垒机 堡垒机品牌

直播卖货APP——为何能得到商家和用户的喜欢?

开源直播系统源码

软件开发 语聊房 直播系统 直播源码

赶紧收藏!!!我直接上瘾!百万人都在学的Docker

指剑

签约计划第三季 8月月更

从零开始,如何拥有自己的博客网站【华为云至简致远】

IT资讯搬运工

linux 文件权限控制

兆骑科创创业赛事活动举办平台,投融资对接,线上直播路演

兆骑科创凤阁

大数据培训课程哪个好呢?

小谷哥

web前端培训班哪个好选择

小谷哥

如何选择ui设计机构

小谷哥

前端培训机构课程怎么样

小谷哥

电商秒杀系统架构设计

泋清

#架构训练营

有了国产 DevOps 工具 ,还怕数字化转型成本高?

SoFlu软件机器人

秋招冲刺版!奉上[Java一线大厂高岗面试题解析合集]

冉然学Java

面试 面试题 大厂 java; 秋招

设计一个跨平台的即时通讯系统(采用华为云ECS服务器作为服务端 )【华为云至简致远】

IT资讯搬运工

云服务器ECS

再迎巅峰!阿里爆款分布式小册开源5天Github已73K

冉然学Java

架构 分布式 微服务 java; 编程、

LED显示屏在会议室如何应用

Dylan

LED显示屏 led显示屏厂家

滴滴出行安卓端 finalize time out 的解决方案_文化 & 方法_江义旺_InfoQ精选文章