【AICon 全球人工智能与大模型开发与应用大会】改变 AI 时代下写代码的模式 >>> 了解详情
写点什么

滴滴出行安卓端 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:133270

评论 1 条评论

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

数据平台调度升级改造 | 从Azkaban 平滑过度到 Apache DolphinScheduler 的操作实践

Apache DolphinScheduler

Apache 大数据 开源 workflow

详细视图——基于函数的视图 Django

海拥(haiyong.site)

Python django 6月月更

Node.js实用的内置API(二)

devpoint

node.js utils 6月月更

fastposter v2.8.3 发布 电商海报生成器

物有本末

Java Python 海报 海报生成

揭开SSL的神秘面纱,了解如何用SSL保护数据

郑州埃文科技

数据安全 SSL证书 IP溯源

去中心化交易所套利机器人开发技术

薇電13242772558

区块链 去中心化

福昕软件重磅发布福昕高级PDF编辑器12.0

联营汇聚

快速玩转CI/CD图形化编排

Jianmu

DevOps 前端 CI/CD 自动化运维 图形化编排

斗栱云杜文宝:如何用一款SaaS改变建筑行业?

ToB行业头条

Spring那点事

飞天

6月月更

什么是网络拓扑?网络拓扑有哪些类型?

wljslmz

网络技术 6月月更 网络拓扑

大数据培训之Flink CEP 的简介

@零度

大数据 flink CEP

Spring Security:用户和Spring应用之间的安全屏障

华为云开发者联盟

安全 防火墙 spring security 华为云

游戏源代码开发时需要什么,需要哪些团队成员?

开源直播系统源码

软件开发 游戏开发 直播源码

大数据工业界解决方案

Joseph295

el-table 分页全选功能讲解

CRMEB

快速认识 WebAssembly

devpoint

rust webassembly Wasm 6月月更

Fabric.js 控制元素层级 👑

德育处主任

前端 canvas Fabric.js 6月月更

【CVPR2022】用于域适应语义分割的域无关先验

华为云开发者联盟

人工智能 华为云 图像域

OceanBase Meetup第五期 复杂业务场景下的数据库应用需求及挑战

OceanBase 数据库

特别干的干货!!《Mycat》搭建分布式数据库中间件看他就够

迷彩

mycat 分布式数据库中间件 6月月更

安擎人工智能计算中心解决方案助推“城市大脑”建设

科技热闻

电竞迎来“新四化”,数字化产业变革正当时

科技之家

web前端培训 | 面试中Vue的各种原理分享

@零度

Vue 前端开发

Vue-15-事件绑定

Python研究所

6月月更

一个老开源人的自述-如何干好开源这件事

云智慧AIOps社区

开源 前端 开源项目 数据可视化

K8s的负载均衡与配置管理

Damon

云原生 k8s 6月月更

并发数、并发以及高并发分别是什么意思?

行云管家

高并发 并发 堡垒机 IT运维 并发数

Java—指令重排序

武师叔

6月月更

如何保证数据库和缓存双写一致性?

C++后台开发

数据库 redis 缓存 中间件 后端开发

7天免费入门数据智能,“2022数据智能夏令营”开启报名!

个推

人工智能 大数据 数据智能

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