从 Android OOM监控与分析

简单介绍

What's OOM?

全称“Out Of Memory”,指的是内存超出了限制,抛出的一个异常。

Why OOM?

  • 内存泄漏,导致可回收的内存回收失败,比如Acitvity已经Destory了,但是还继续被引用,导致无法回收。
  • 程序运行所需要的内存大于给定的内存,比如加载一张巨大的高清的图片资源,立马OOM。
     

常见的OOM

类型为java.lang.OutOfMemoryError: Java heap space ,Java堆内存溢出。

一般的解决办法

Java抛出来的OutOfMemoryError,和其他崩溃一样也会有其堆栈信息。如下

java.lang.OutOfMemoryError: Failed to allocate a 4308492 byte allocation with 467872 free bytes and 456KB until OOM

at dalvik.system.VMRuntime.newNonMovableArray(Native Method)

at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)

at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:609)

at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:444)

at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:988)

at android.content.res.Resources.loadDrawableForCookie(Resources.java:2580)

at android.content.res.Resources.loadDrawable(Resources.java:2487)

at android.content.res.Resources.getDrawable(Resources.java:814)

at android.content.res.Resources.getDrawable(Resources.java:767)

通过堆栈信息能确认到崩溃那一刻调用的方法,在本例子中可以看到是解析加载图片资源时出错。但是我们并不知道是哪个图片,而且这图片并不是真正的问题所在,因为这个时候内存已经达到了接近崩溃的边缘,做任何分配的操作都会导致OOM。

所以在解决该类问题时,最常用的办法就是heapdump,这是java虚拟机上的应用解决OOM的通用办法。

将OOM时的内存dump下来后,通过Memery Analyzer( MAT)工具打开进行分析,MAT的使用可以参考[分析指南]

如何dump内存

针对debuggable的应用,调用

adb shell am dumpheap <processname> <FileName>

对于release的应用,只能Java代码中使用

Android.os.Debug.dumpHprofData(filePath)Android.os.Debug.dunpHprofData()

Android获取到的内存文件不能直接用于MAT分析,需要通过Android SDK提供的工具进行转换

android-sdk/platform-tools/hprof-conv <FileName> <newFileName>.hprof

上面就是一般的分析流程,而我们需要的是一套完善的更加自动化的方案。

有几个需要解决的问题

  • 端上内存自监控
  • 端上内存主动释放
  • 端上dump时机
  • 端上问题自动发现
     

监控阶段

端上内存自监控

为什么要自己监控内存?因为在全局异常捕获中收到OOM error信息时再去做内存dump,会有一定的失败率。dump还未完成,程序就已经崩掉,所以如果能在抛出OOM error,及早发现可能OOM,这个时候去做一些处理,能有更多的操作空间。

具体方案为在监控开始时,起一个定时线程,每隔3秒进行一个内存监测

内存占用计算:(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())/Runtime.getRuntime().maxMemory()

  • totalMemory: Returns the total amount of memory in the Java virtual machine.
  • freeMemory:Returns the amount of free memory in the Java Virtual Machine.
  • maxMemory:Returns the maximum amount of memory that the Java virtual machine will attempt to use.
     

端上内存主动释放

当能监控到应用运行过程中的占用比时,能做的事情就比较多了,可以选择在高内存占比时,先做一些释放操作,在基线里面目前做了释放了两个模块,图片和Card,因为这两个模块是使用内存的大头。

现在的策略是在不同的占比做不同程度的内存释放。

系统也会监控内存,提供了内存的回调

  • OnLowMemory

    在系统内存不足并且所有后台程序已经被杀死时,会调用该方法
  • OnTrimMemory

    系统会根据不同的内存状态来回调

系统的这两个回调也应该做一些释放,这个和我们的监控不冲突,或者说是互补,因为一个是系统内存,一个是应用内存。

端上dump时机

因为dump是非常影响用户体验的,最好要在真正需要dump的时候进行

当内存达到97%(这个是目前经验值)时,我们判断其已经接近OOM,是不是dump时机,还不能保证。因为可能有20%的内存在下次gc就能被回收掉

为了在gc之后再看看情况,流程中会主动调用System.gc(),熟悉的同学都知道gc()并不是你调用就马上执行,这是由虚拟机决定的。

通过查找资料,并没有直接的方式去判断虚拟机是否执行了我们的GC,我们也必须在GC后看看内存是否还在97%,这表示内存已达到了无可释放的程度。

现在是通过间接的方式来观察GC情况,Java引用类型分为四种强引用,软引用,弱引用,虚引用,每个的描述我就不介绍了,用到的就是弱引用,弱引用特点是当遇到gc时就会立马回收掉。

在首次内存达到97%的时候,创建一个弱引用对象作为哨兵,然后调用gc。当下次查看内存,发现哨兵被回收且内存继续保持97%的时候,我们就可以做dump操作了。

dump的性能问题

dump会导致应用进程冻结一段时间,会给用户带来比较明显的卡,经过测试在android 7.0以后的系统能降低到5s以内,而7.0以前的系统会卡10s及10s以上,所以在线上运行是我们是采样并且过滤版本的方式,样本控制的尽量少,以发现问题为主要目的。

内存泄漏加成

除了监控内存做相应的处理,OOM也会配合内存泄漏监控,为后续的分析问题提供更多的信息

简单介绍下内存泄漏监控

首先registerActivityLifecycleCallbacks注册Activity的生命周期回调
然后在onActivityCreated,onActivityDestroyed记录每个Activity的开始和中止

当一定时间后发现有Activity已经被destroyed但是还未被回收,便认为泄漏,在应用要dump的同时会将该泄漏信息保留下来,具体的可以参考LeakCanary发现原理

图1: 监控,及发生OOM后做的响应应急处理流程

处理阶段

端上问题自动发现主要是依赖的二次启动来处理,在dump完之后应用大概率会崩溃,而分析那些逻辑主要在第二次启动

第二次启动后发现有dump的文件,便开始进入处理分析流程,这里是另外起了一个:memery进行执行

分析流程有如下图三种处理方式

图2 问题分析

解析泄漏路径

如果同时有泄漏信息,直接调用Leakcanary进行路径分析(原理参考LeakCanary的分析原理),并投递

大内存解析

如果主要分析dump信息中包含的大内存文件,关于这一点可以参考OOMTrack的PPT的介绍

压缩上传

当未发现泄漏信息时,会有随机抽取50%的样本采取压缩上传的方式,因为大内存解析并不能准确发现所有问题,这个时候就能手动导入MAT进行更全面的分析了。

展示

Qmas数据展示

上面两张图绿色部分的投递,最终都能在Qmas中查看

Qmas对应分析入口地址:http://qmas.qiyi.domain/embed/apm?mod=crash -> 内存分析 -> OOM分析

后续

遗留问题

现在OOM的分析有三个问题待后续解决,dump带来的卡住时间问题,分析耗时问题,文件即使压缩也很大的问题

目前业内也已经有解决方案,其实质是裁剪dump的冗余信息

后续方案

利用xHook,在执行dump的准备阶段,我们会调用Native层的open函数获得一个文件句柄,但实际执行时会进入到Hook层中,然后将返回的FD保存下来,用作write时匹配。

在dump开始时,系统会不断的调用write函数将内容写入到文件中。由于我们的Hook是以so为目标的,系统运行时也会有许多写文件的操作,所以我们需要对前面保存的FD进行匹配。若FD匹配成功则进行裁剪,否则直接调用origin-write进行写入操作。

裁剪后的文件可以变得很小,而且因为dump的io负担变小,也能极大的减少dump卡住时间,并且分析耗时也会随着文件变小而减少

通过这种方式,以上三个问题都能很好的优化

但是确定哪些内容需要裁剪,裁剪后如何再次解析,如何兼容MAT工具,都需要投入一定的成本去完成,目前还未计划支持。

可参考的文章Probe:Android线上OOM问题定位组件

基线APM内存分析接入 APM 内存监控分析

おすすめ

転載: blog.csdn.net/cpcpcp123/article/details/121655510