马上: Android线上OOM问题定位解决分享及总结

13 篇文章 0 订阅
5 篇文章 0 订阅

项目背景

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.

关键堆栈不一一贴出了.

上面几种报错信息,简单分析一下

  1. 创建线程失败,栈内存不足(进程的虚拟内存不足)或超线程数,
  2. 创建线程失败,超FD(文件描述符)或mmap创建匿名共享内存时(也是进程的虚拟内存不足)
  3. 创建资源时,堆内存分配失败
  4. 创建资源时,被try Throwable,堆内存分配失败,导致stack 溢出
  5. 游标创建失败,堆内存分配失败

再归档一下项目中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 等。

在项目解决了:

  1. Activity/Fragment 的泄露问题
  2. rxjava CompositeDisposable 泄露,clear 并不等同dispose , add(Disposable) 函数,DisposeTask执行完成,必须及时移出 remove(Disposable)
  3. 没有及时反注册引起的资源泄露,用WeakHashMap 效果很好
  4. 旧业务采用volley,NetworkDispatcher.run() ,请求队列轮询一直持有request,升级为okhttp 解决或者升级最新sdk
  5. 下载器Cancelable 对象泄露,Map<String, Cancelable> mCancelDownloading 没有及时移出导致
  6. ijk播放器在轮播页不断创建泄露,native fd 泄露
  7. Toast 泄露
  8. …不一一列举

以上上面只是通过日志分析场景,通过复现现场,抓取内存快照来分析解决。此方式耗费时间成本,为此项目中监控了堆内存、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:两个文件对比 


总结

  1. 解决问题,必须要有扎实学识。如OOM,需要掌握Java内存分配,回收;Linux的FD文件描述符;线程底层创建原理
  2. 工欲善其事,必先利其器。线上监控,日志回捞方案,掌握profiler,mat工具使用等必不可少。
  3. 多分析相关的代码,找出相应的问题关键,再来考虑具体的优化策略。
  4. 优化完代码,要不断自测,保持一颗敬畏的心。
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值