@TOC
项目背景
1、由于马上智能终端App要为用户提供了24小时不间断的服务特性,App对于应用稳定性的要求非常高,体现App稳定性的一个重要数据就是Crash率,而在众多Crash中最棘手最头疼最难定位的就是OOM问题。
2、对于智能终端设备来说, 在长时间的使用过程中,App中所有的内存泄漏都会慢慢累积在内存中,最后就容易导致OOM,进而影响整个自助服务。
3、OOM是软件领域的经典问题,它藏得很深,没太多征兆,但爆发问题,问题来源的多样、不易重现、现场信息少、难以定位等困难
线上现状
腾讯Bugly,为移动开发者提供专业的异常上报和运营统计,帮助开发者快速发现并解决异常,同时掌握产品运营动态,及时跟进用户反馈。
采用 腾讯bugly 分析现状,发现OOM发生机率之高
OOM原因分析
要定位OOM问题,首先需要弄明白Android中有哪些原因会导致OOM,Android中导致OOM的原因主要可以划分为以下几个类型:
结合现状buggly堆栈报错信息
- pthread_create (1040KB stack) failed: Try again
- Could not allocate JNI Env
- allocate a 7687692 byte allocation with 2774696 free bytes and 2MB until OOM
- OutOfMemoryError thrown while trying to throw OutOfMemoryError; no
stack available - Cursor window allocation of 2048 kb failed.
关键堆栈不一一贴出了.
上面几种报错信息,简单分析一下
- 创建线程失败,栈内存不足(进程的虚拟内存不足)或超线程数,
- 创建线程失败,超FD(文件描述符)或mmap创建匿名共享内存时(也是进程的虚拟内存不足)
- 创建资源时,堆内存分配失败
- 创建资源时,被try Throwable,堆内存分配失败,导致stack 溢出
- 游标创建失败,堆内存分配失败
再归档一下项目中OOM情况:
1、可能线程泄露,线程数超出限制
2、可能文件资源泄露,FD数超出限制
3、对象泄露,java堆内存不足
OOM问题定位
分析线上问题内存泄露,排查的一个难点:如何定位,复原案发现场
- 分析buggly
- 查看发生崩溃的时间,崩溃时设备的情况,一般会记录闪退日志和重要日志记录
- 查看使用使用时长,业务场景下操作复现
1、buggly先简单定位到用户ID,用户使用时长,可用系统内存,发生时间。可以先根据堆栈信息来确定这是哪一个类型的OOM,再进行日志回捞和业务场景复现
2、日志回捞分析 (需要平台支持下的一套日积月累成熟方案)
项目中我加入了CPU,内存,FD,NetworkInfo,ThreadsInfo等关键模块监控,关键日志信息埋点
CpuInfo: User 6%, System 6%, IOW 0%, IRQ 1%
MemoryInfo: 1.95G,1.35G,144.00M,false; JavaHeapInfo: 38/712mb,ratio:0.05%
FdInfo: fd size: 172
StatusInfo: Threads: 120 voluntary_ctxt_switches: 1029455 nonvoluntary_ctxt_switches: 56904
NetworkInfo: type: Ethernet[9], subtype: [0], 8e:a2:0c:64:58:52, 10.0.6.144
充分了解项目基本情况,比如该项目中fd 数量200+,和线程数200+,堆内存占比30%是合理的。下面是个真实例子
回捞的日志信息,发生时间为12-18 13:38
12-18 13:38:24.747: : E/14491/MGException: Id=NHG47K&Display=v1.0.5&Product=rk3288&Device=rk3288&Board=rk30sdk&CpuAbi=armeabi-v7a&CpuAbi2=armeabi&Manufacturer=Haitianxiong&Brand=Haitianxiong&Model=VX-3288K&Hardware=rk30board&Serial=VCPIDLDV6Z&Type=userdebug&Tags=test-keys&FingerPrint=Haitianxiong/rk3288/rk3288:7.1.2/NHG47K:userdebug/test-keys&Version.Incremental=eng.root.20200713.104251&Version.Release=7.1.2&SDK=25&SDKInt=25&Version.CodeName=REL&Density=0.75;Width=800;Height=444;ScaledDensity=0.75;xdpi=213.0;ydpi=213.0;DensityDpi=120&Ver=6.9.6_211108
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:730)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)
at java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)
12-18 13:38:24.749: : D/14491/DefaultUncaughtExceptionHandler: appCrashTimesStr:1639780371570,1639786813250,1639793211423,1639799591147,1639799591744
12-18 13:38:24.749: : D/14491/DefaultUncaughtExceptionHandler: crashCount:0
12-18 13:38:24.750: : D/14491/DefaultUncaughtExceptionHandler: appCrash : 借助其它app 启动
上面创建线程失败,分析Threads信息基本模块日志,13:38 期间线程数达限制导致失败,原因是不断创建线程,线程泄露
12-18 11:53:19.068: : D/14491/StatusInfo: Threads: 108 voluntary_ctxt_switches: 764 nonvoluntary_ctxt_switches: 285
12-18 12:03:19.526: : D/14491/StatusInfo: Threads: 177 voluntary_ctxt_switches: 72915 nonvoluntary_ctxt_switches: 6952
12-18 12:13:19.943: : D/14491/StatusInfo: Threads: 239 voluntary_ctxt_switches: 125722 nonvoluntary_ctxt_switches: 10480
12-18 12:23:20.337: : D/14491/StatusInfo: Threads: 296 voluntary_ctxt_switches: 201087 nonvoluntary_ctxt_switches: 16183
12-18 12:33:20.726: : D/14491/StatusInfo: Threads: 352 voluntary_ctxt_switches: 253078 nonvoluntary_ctxt_switches: 19911
12-18 12:43:21.141: : D/14491/StatusInfo: Threads: 405 voluntary_ctxt_switches: 315668 nonvoluntary_ctxt_switches: 25419
12-18 12:53:21.582: : D/14491/StatusInfo: Threads: 460 voluntary_ctxt_switches: 375660 nonvoluntary_ctxt_switches: 29996
12-18 13:03:22.056: : D/14491/StatusInfo: Threads: 517 voluntary_ctxt_switches: 428586 nonvoluntary_ctxt_switches: 34298
12-18 13:13:22.508: : D/14491/StatusInfo: Threads: 573 voluntary_ctxt_switches: 466703 nonvoluntary_ctxt_switches: 36509
12-18 13:23:22.937: : D/14491/StatusInfo: Threads: 628 voluntary_ctxt_switches: 504824 nonvoluntary_ctxt_switches: 38960
12-18 13:33:23.394: : D/14491/StatusInfo: Threads: 682 voluntary_ctxt_switches: 542957 nonvoluntary_ctxt_switches: 41872
12-18 13:38:32.157: : D/24733/StatusInfo: Threads: 105 voluntary_ctxt_switches: 705 nonvoluntary_ctxt_switches: 263
12-18 13:48:32.736: : D/24733/StatusInfo: Threads: 180 voluntary_ctxt_switches: 113782 nonvoluntary_ctxt_switches: 14680
再看看内存使用情况
12-18 13:03:22.048: : D/14491/MemoryInfo: 1.95G,1.33G,144.00M,false
12-18 13:13:22.505: : D/14491/MemoryInfo: 1.95G,1.33G,144.00M,false
12-18 13:23:22.933: : D/14491/MemoryInfo: 1.95G,1.33G,144.00M,false
12-18 13:33:23.391: : D/14491/MemoryInfo: 1.95G,1.32G,144.00M,false
12-18 13:38:30.627: : I/24817push/MemoryInfo: ESS=mounted;ESD=/storage/emulated/0;ESSC=mounted;EXIST=true,true,false,false,true,false,false&IMA=2.48G;IMT=3.91G;EMA=2.48G;EMT=3.91G&AM.MEM=1.52G,1.95G,144.00M,false
12-18 13:38:32.155: : D/24733/MemoryInfo: 1.95G,1.50G,144.00M,false
12-18 13:48:32.732: : D/24733/MemoryInfo: 1.95G,1.34G,144.00M,false
结论是这个OOM,是线程使用不合理,根据业务日志和结合代码分析出,是接入的sdk循环初始化导致OOM
Fd泄露,类似线程泄露,找出项目代码哪里没有释放fd
12-16 07:10:23.143: : D/1308/FdInfo: fd size: 193
12-16 07:20:23.591: : D/1308/FdInfo: fd size: 183
对象泄露导致堆内存不足, 需要根据项目分析出现的场景操作复现。
MemoryInfo: 1.95G,739.30M,144.00M,false; JavaHeapInfo: 500/512mb,ratio:97%
OOM问题分析
OOM问题定位到原因,但是要结合项目具体分析修复
前面OOM问题定位,已通过回捞日志分析并模拟发生场景,接下来通过官方分析应用性能工具之android-studio profile ,生成Java内存快照文件(即HPROF文件)。
android-studio bin目录下,可以单独允许运行
也可以打开as,profiler视图
查看Show activity/fragment leaks,这功能直接分析 activity/fragment 泄露地方,
然后结合业务,这是智慧屏的一个Metro风格展示,上面泄露的是轮播的磁贴Fragment
Banner业务实体类代码分析,居然引用了fragment, 万一哪里没有setFragment(null),或者持有Banner,Fragment对象就泄露了
可是项目复杂想·业务代码几千行,知道Fragment 被
mBanners
持有着,一时间也无从下手,可以先用WeakReference 引用尝试定位,发现确实是这个Banner引起。
setFragment
业务代码埋点日志, 并输出调用栈,新预览版as,能查看调用栈,方便挺多的
Log.i("aaa", "setFragment: " + fragment + " ;" + Utility.getStackTraceElement(4));
结果日志埋点后发现了:轮播主页Fragment的Banners初始化有以下问题:
1、mBanners
clear时是没有把Banner里的Fragment对象清除.
2、无用的Banner的Fragment没有清除
private ArrayList<Banner> mBanners;
// 刷新ViewPager leak代码
if (mBanners != null && !mBanners.isEmpty()) {
mBanners.clear();
}
// 刷新ViewPager 修复leak代码
if (mBanners != null && !mBanners.isEmpty()) {
for (Banner banner : mBanners) {
if (banner == null) {
continue;
}
banner.setFragment(null);
}
mBanners.clear();
}
//外部磁贴最多只能轮播5帧 leak代码
ArrayList<Banner> subList = new ArrayList<>(5);
subList.addAll(tmpBanners.subList(0, 5));
pagesBanner.setPages(subList);
boolean removeAll = tmpBanners.removeAll(subList); //添加修复leak代码
if (removeAll && tmpBanners != null) {
Iterator<Banner> it = tmpBanners.iterator();
while (it.hasNext()) {
Banner banner = it.next();
if (banner.getView() != null) {
continue;
}
Fragment next = banner.getFragment();
if (next != null && !next.isAdded()) {
Banner banner1 = banner;
if (banner1 != null) {
banner1.setFragment(null);
}
}
}
}
一般情况下通过,profiler来分析堆内存, 能定位项目中的Activity,Fragment泄露原因.
如界面销毁时Handler
没有及时移除消息。不合理使用Fragment
, replace fragment
没有用tag和remove
等。
在项目解决了:
- Activity/Fragment 的泄露问题
- rxjava
CompositeDisposable
泄露,clear
并不等同dispose
,add(Disposable)
函数,DisposeTask执行完成,必须及时移出remove(Disposable)
- 没有及时反注册引起的资源泄露,用
WeakHashMap
效果很好 - 旧业务采用volley,
NetworkDispatcher.run()
,请求队列轮询一直持有request,升级为okhttp 解决或者升级最新sdk - 下载器
Cancelable
对象泄露,Map<String, Cancelable> mCancelDownloading
没有及时移出导致 - ijk播放器在轮播页不断创建泄露,native fd 泄露
- ...不一一列举
以上上面只是通过日志分析场景,通过复现现场,抓取内存快照来分析解决。此方式耗费时间成本,为此项目中监控了堆内存、FD、Thread使用指标,达到这指标并且是关注设备通过Debug.dumpHprofData(String fileName),获取快照文件,裁剪回捞HPROF文件等工作,虽然成功率不高,也能节约大量时间分析现场
使用MAT分析复杂的OOM情况
Android studio 分析内存堆的profiler 工具,简单便利。但是缺少比对,查对象引用等功能,而MAT提供了非常多的功能。
Memory Analyzer Tool 是一个分析 Java堆数据的专业工具,可以计算出内存中对象的实例数量、占用空间大小、引用关系等,看看是谁阻止了垃圾收集器的回收工作,从而定位内存泄漏的原因。
使用MAT之前,需要认识:
- Java内存分配策略 ,静态存储区(方法区)+栈区+ 堆区
- Java管理内存的机制,GC机制 (有向图)
- Java内存泄漏,对象对象是可达的,即在有向图中,存在通路可以与其相连且以后不会再使用这些对象
- Android sdk hprof-conv, 把安卓hprof文件转换为标准的java hprof文件
关键词概念
- Dominator:从GC Roots到达某一个对象时,必须经过的对象,称为该对象的Dominator。
- ShallowSize:对象自身占用的内存大小,不包括它引用的对象。
- RetainSize:对象自身的ShallowSize和对象所支配的(可直接或间接引用到的)对象的ShallowSize总和,就是该对象GC之后能回收的内存总和。
Histogram:直方图,可以列出内存中每个对象的名字、数量以及大小。
Dominator Tree:会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。
Group分组功能,工具栏的 Group result by...
List objects:想要看某个条目(对象/类)的引用关系图,可以使用 List objects 功能
List objects -> with outgoing references :表示该对象的出节点(被该对象引用的对象)
List objects -> with incoming references:表示该对象的入节点(引用到该对象的对象)
分析引用链路径:
Paths to GC Roots:从当前对象到GC roots的路径,这个路径解释了为什么当前对象还能存活,对分析内存泄露很有帮助,这个查询只能针对单个对象使用
Merge Shortest Paths to GC roots:从GC roots到一个或一组对象的公共路径
排除泄露选-> exclude all phantom/weak/soft etc. references,因为GC无法回收的强引用对象
Add Compare Basket 或者Compare to another heap dump:两个文件对比
总结
- 解决问题,必须要有扎实学识。如OOM,需要掌握Java内存分配,回收;Linux的FD文件描述符;线程底层创建原理
- 工欲善其事,必先利其器。线上监控,日志回捞方案,掌握profiler,mat工具使用等必不可少。
- 多分析相关的代码,找出相应的问题关键,再来考虑具体的优化策略。
- 优化完代码,要不断自测,保持一颗敬畏的心。