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

@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.

关键堆栈不一一贴出了.

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

  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目录下,可以单独允许运行


image

也可以打开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. ...不一一列举

以上上面只是通过日志分析场景,通过复现现场,抓取内存快照来分析解决。此方式耗费时间成本,为此项目中监控了堆内存、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. 优化完代码,要不断自测,保持一颗敬畏的心。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容