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

项目背景

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. 优化完代码,要不断自测,保持一颗敬畏的心。
### 关于线OOM (Out Of Memory) 案例分析及解决方案 #### 线上环境中的OOM问题概述 在线上环境中,当应用程序遭遇 Out of Memory (OOM) 错误时,通常会调用 `Thread::ThrowOutOfMemoryError` 函数并传递描述错误详情的消息参数 msg[^1]。这类异常不仅影响用户体验还可能导致服务中断。 #### 实际案例解析 假设某 Android 应用程序频繁出现崩溃现象,在日志中发现大量由系统抛出的 OOM 异常记录。进一步调查表明该应用存在不合理加载图片资源的情况——即一次性尝试加载过多高分辨率图像至内存中而未做适当优化处理。这使得虚拟机无法分配足够的连续空间来满足请求从而触发了 OOM 错误。 针对上述情况采取如下措施: - **减少单次加载量**:限制每次仅读取一定数量的小尺寸缩略图而非原始大小; - **启用缓存机制**:对于已加载过的图片实施 LRU 缓存策略以便重复访问时不需重新获取; - **异步操作**:采用后台线程完成耗时较长的任务如网络请求或磁盘IO动作防止阻塞主线程造成响应延迟甚至卡死状况的发生。 经过以上改进之后有效地缓解了因图片加载不当所引发的一系列性能瓶颈问题显著降低了 OOM 发生概率提升了整体稳定性表现。 #### 工具辅助诊断流程 面对较大规模的应用程序,手动排查可能存在效率低下且难以全面覆盖所有潜在风险点的问题。此时可以借助专业的调试工具来进行更深入细致地剖析工作。例如 JVisualVM 是一款功能强大的 Java 应用性能监控平台能够帮助开发者快速定位到具体哪一部分代码消耗了大量的堆内存量进而指导后续修复方向的选择不过需要注意的是如果待检测的数据集非常庞大则建议预先调整好 JVM 的启动参数以确保有足够的可用 RAM 来支持整个分析过程顺利开展[^2]。 另外还可以考虑使用其他专门用于 heap dump 文件解析的专业软件比如 Eclipse MAT 或 Visual VM 自身集成的功能模块等它们各自具备独特的优势可以根据实际需求灵活选用最合适的选项。 #### 内存泄漏预防指南 为了避免未来再次遇到类似的挑战可以从以下几个方面着手加强防护力度: - 定期审查现有架构设计是否存在不必要的对象持有关系特别是静态成员变量以及监听器注册注销逻辑是否严谨无遗漏之处; - 对第三方库保持警惕谨慎引入未经充分测试验证的新依赖项以免埋下隐患; - 培养良好的编程习惯遵循最佳实践编写易于维护扩展性强的高质量源码。 ```java // 示例代码展示如何安全释放Bitmap资源 public void recycleBitmap(Bitmap bitmap){ if(bitmap != null && !bitmap.isRecycled()){ bitmap.recycle(); System.gc(); // 提示垃圾回收器尽快清理不再使用的对象 } } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值