内存监控
程序开发活动中,内存管理是特别复杂的一项任务,而大多数的疑难Bug都或多或少与内存有关,针对内存问题,我们可以简单的分为以下三类:内存泄漏、内存滥用、内存访问异常:
内存泄漏: 本应该被释放的内存没有被释放,就会造成内存泄漏问题,泄漏发生后,进程的内存会持续增长,最终导致OOM等问题;
内存滥用: 利用缓存等机制提升程序性能是一种常见的优化手段,但是其有效性一般也要看对缓存对象的管理,稍有异常可能会缓存很多根本用不上的对象,这样不仅不能优化性能,而且还会导致其他模块申请不到内存,从而导致一些内存相关的性能问题,例如GC卡顿等;
内存访问异常: 内存访问异常,就是我们常见的段错误、内存越界访问这类问题,这一类问题一般都比较难分析,例如内存越界问题,可能在内存越界的当下并没有发生什么异常,只是把某些地址的值写坏了,而当后面其他模块用到这块内存的时候才会触发像段错误这类问题,但由于不知道是谁把内存写坏的,所以即使知道段错误的堆栈一般也无济于事;
无论哪种内存问题,都会严重影响用户的基础体验,例如闪退、黑屏、卡顿等,所以对内存问题的监控是很重要的,然而对内存问题的监控和识别本身又是一件极其困难的事情(一是因为内存的使用场景非常频繁,常规监控手段容易受限,另外一个内存对象是否是滥用得结合用户业务逻辑来看,比较繁琐),对此,Bugly的内存问题的监控主要分为 异常监控 和 内存治理 两部分,像Activity泄漏、内存越界、大图这种大家都认为是异常的情况,Bugly会给出准确的日志信息,用户可以直接提单跟进,例如Activity的泄漏链或者内存越界的堆栈等,另一面也有提供像Java内存详情、FD详情、Native内存详情等偏内存治理的能力,Java内存详情、FD详情等能力会在Java内存、FD资源等快要耗尽的时候,抓取日志信息上报后台,然后分析出疑似的问题,例如超大Java对象的引用链或者打开FD资源过多的堆栈信息,用户可以通过这些信息直接或者进一步分析得到问题的根因。
一、 内存指标
内存指标是针对大盘用户的统计数据,可以通过指标数据来观察应用整体内存使用情况,另外也可以通过横向或者纵向对比指标,观察内存指标是否有劣化等等,目前Android的内存监控指标主要包括:OOM率、内存峰值、FD触顶率 这几个数据,可以通过页面左侧OOM、内存的 指标分析 页面来查看;
1.1 OOM率
OOM问题相信大家都很熟悉,例如 java.lang.OutOfMemoryError 异常,Dalvik和ART虚拟机的Java堆内存都有一个使用上限,如果需要分配的内存超过这个限制,虚拟机就会上报 java.lang.OutOfMemoryError 异常,所以 OOM 的发生一般都与内存有关,但也有些例外,例如在Android中创建Java线程的时候,有一个逻辑需要用到FD资源,如果这个时候FD资源不够了,虚拟机也会上报为java.lang.OutOfMemoryError异常,同样也是在创建Java线程的时候,需要mmap映射一段内存,如果Native内存地址空间不够,也会上报OOM,所以如果将 ”java.lang.OutOfMemoryError“ 异常与Java内存划等号是不正确的,为此我们拓展了OOM问题的含义,我们会将由于 虚拟机堆内存耗尽、FD资源耗尽、Native内存耗尽 导致的崩溃问题分别归属为 Java OOM、FD OOM、Native OOM, 对应这三种OOM问题提供三种OOM率:
- Java OOM率:虚拟机Java堆内存耗尽导致的崩溃问题概率,衡量Java内存使用带来的崩溃问题影响大小;
- Native OOM率:进程Native内存耗尽标导致的崩溃问题概率,衡量Native内存使用带来的崩溃问题影响大小;
- FD OOM率:进程FD资源耗尽导致的崩溃问题概率,衡量FD资源使用带来的崩溃问题影响大小;
您可以进入到OOM的”指标分析“ 页面,查看这三种不同的OOM率的详细数据。
设备OOM率 = 影响设备数 / 联网设备数。
次数OOM率 = 发生次数 / 联网设备数 。
人数OOM率 = 影响用户数 / 联网设备数 。
单位时间oom率 = 发生次数 / { oom上报的(crash_time - start_time) 相加总和 / 3600 },即平均每小时内发生 xx 次 oom crash
影响设备数:发生OOM的设备,按设备ID去重统计数量。
影响用户数:发生OOM的用户,按用户ID去重统计数量。
联网设备数:联网设备,按设备ID去重统计数量。
1.2 内存峰值
内存峰值,也就是一个进程生命周期里面达到的最大内存占用值,包含PSS、VSS、JavaHeap(totalMemory - freeMemory)三种分类,任何一台移动设备的可用内存是有限的,当一个进程使用的内存越多,在后台被系统kill掉的风险就越大,GC导致的性能问题影响也会越多,内存峰值这个指标在一定程度上可以衡量这些问题的影响程度,Bugly默认会采集主进程的内存峰值数据,如果想要获取子进程的内存峰值,可以通过如下配置来开启:
进入到内存的指标分析页面,可以查看内存峰值,支持多种下钻手段,例如查看不同场景的内存峰值,或者查看某个用户自定义字段值的内存峰值:
1.3 FD触顶率
跟内存一样,FD也是一种有限的资源,一个进程使用FD资源超过最大值之后,就无法再继续获取,继而会出现闪退、黑屏等问题,特别是一些低版本的Android,一个进程的可用FD资源被限制在1024,这对大型应用来说,1024个FD资源是很难满足需求的,所以有必要监控我们进程中FD资源的使用,FD触顶率是用来衡量开启FD详情功能之后,FD数目达到一个既定阈值的概率,只有在开启FD详情监控这个功能,才会上报这个指标,FD触顶的阈值可以查看下面关于FD详情功能的介绍.
二、个例监控
2.1 Java内存详情
如前面所述,Java内存使用过量后,不仅会触发GC,引起卡顿、ANR等问题,更严重会直接导致OOM闪退,严重影响用户体验,Java内存详情是这样一种能力,开启后,会时刻监控当前虚拟机堆内存使用情况,当超过一个预设值的时候会自动采集相关日志信息上报后台,这个功能主要提供了如下一些服务:
实现了类似MAT的”Top Consumers“的能力,日志信息上报后台后,可以分析出 ”泄漏对象“、”单个大对象“、”密集大对象“ 等业务比较关心的问题; 不需要用户手动抓取hprof文件,然后拖到PC端使用MAT工具分析,极大的提升了分析效率.
在频繁GC或者连续多次达到Java堆内存最大值的某个阈值的时候,在移动端通过子进程自动dump Java堆内存转储文件; 在后台dump堆内存转储文件,对用户体验影响非常小.
可以在线使用,同时利用Bugly已有的翻译能力,翻译类名和成员变量名之后,再智能提取聚类关键特征,将相似的问题聚类到一起; 自动翻译和聚类,让用户可以轻松聚焦Top问题,解决了MAT等工具需要手动翻译的繁琐操作问题.
泄漏对象:目前支持检测泄漏的Activity对象;
单个大对象:类比MAT的 ”Biggest Objects“ 功能,即其Retained Size大于某一个阈值的单个Java对象或者类,该阈值目前由Bugly后台指定;
密集大对象:类比MAT的 ”Biggest Top-Level Dominator Classes“ 功能,即某一Java类,虽然其单个对象小,但是其所有的Java对象实例的总Retained Size和超过某一个阈值,该阈值目前由Bugly后台指定;
2.1.1 开启方式
需要升级到 Bugly SDK 4.4.2.2 之后的版本才支持Java内存详情监控功能,客户端需要先执行如下方式代码开启,同时在后台调整采样率配置来开启:
- 客户端
在初始化Bugly的时候,添加如下代码
public static void initBugly(Context context) {
// 1. 初始化参数预构建,必需设置初始化参数
String appID = "a278f01047"; // 【必需设置】在Bugly 专业版 注册产品的appID
String appKey = "1e5ab6b3-b6fa-4f9b-a3c2-743d31dffe86"; // 【必需设置】在Bugly 专业版 注册产品的appKey
BuglyBuilder builder = new BuglyBuilder(appID, appKey);
......
// 2. 开启Java内存详情
build.addMonitor(BuglyMonitorName.MEMORY_JAVA_CEILING);
// 3. 初始化,必需调用
Bugly.init(context, builder);
}
- 配置项
sample_ratio: 控制用户采样率,即多少设备会开启这个功能;
event_sample_ratio: 控制事件采样率,即发生Java内存触顶之后,是否需要上报;
threshold:设置日志文件的抓取时机,90 代表的是在达到堆内存最大值的90%的时候开始抓取日志,堆内存最大值对应 Runtime.getRuntime().maxMemory();
2.1.2 接口说明
暂无
2.1.3 日志说明
1、功能开启成功的日志:
07-04 10:xx:xx.xxx 14546 1819 D RMonitor_MemoryCeiling: Start MemoryCeilingMonitor
07-04 10:xx:xx.xxx 14546 1819 D RMonitor_MemoryCeiling: start detect memory ceiling
2、检测到Java内存触顶,开始dump日志文件:
07-04 10:xx:xx.xxx 3713 3713 I .example.sdkapp: hprof: heap dump "/storage/emulated/0/Android/data/com.example.sdkapp/files/Tencent/RMonitor/main/Log/dump_LowMemory_24-07-04_10.20.41.hprof" starting...
3、上报日志文件到Bugly后台的日志(WiFi网络下实时上报,其他网络重启后上报):
07-04 10:xx:xx.xxx 14546 1918 I RMonitor_report_File: url: https://xxx.qq.com/v1/xxxx/upload-file?timestamp=1720059647340&nonce=7153357010e6227230d5deb79ce73ed7, sub_type: java_memory_ceiling_hprof
2.1.4 Web功能说明
当Bugly后台收到用户的Java内存触顶日志后,会通过自研的堆转储文件分析工具分析出“单个大对象”、“密集大对象”、“泄漏对象”等问题,每个问题都会提取其关键特征,然后根据关键特征来做聚类,所以一次上报一般会对应Bugly的多个issue,在单个大对象和密集大对象的问题列表页面主要包含“筛选项”、“上报趋势”、“问题列表”三项内容,其中
问题列表页 - 单个大对象 & 密集大对象
- 筛选项
虚拟机最大堆内存(MB): 是一个下拉选择框,对应 Runtime.getRuntime().maxMemory()的值; 问题特征:可以通过“匹配”等多种方式来过滤特定特征的issue;
- 上报趋势
样本数量:对应问题列表中个例的数量;
- 问题列表
问题列表按照issue归类,每个issue有自己的关键特征,关键特征主要有三种类型 “引用链”、“支配树“、”纯文本“,引用链是大对象到GC Root的最短路径, 针对大对象是文件或者线程的,会以文件路径或者线程名字来作为聚类特征。
密集大对象的聚类特征即为该大对象的类名:
问题详情页
单个大对象和密集大对象有对应的问题详情页,从该页面我们可以看到大对象的所有GC Root引用链和它的内存支配树,为了减少展示的层级,支配树默认只会展示大于父节点大小10%的子节点内容,如果叶子节点是数组,还会继续打印数组的元素,同时在附件tab中,可以下载原始的堆转储hprof文件,可以通过hprof-conv工具转换后,通过MAT等工具本地分析。
问题列表页 - 单次上报分析 单个大对象和密集大对象单的问题列表展示的是分析和聚类后的结果,而单次上报分析的问题列表是没有聚类的,用户的每一条上报在问题列表中会单独占一条,但是点开其中一条后,可以展示该次上报里面是否有内存泄漏问题以及所有的单个大对象和密集大对象问题,可以很方便的查看某次Java内存触顶的详细原因,同时也提供批量下载堆转储hprof文件的功能。
2.2 Java内存泄漏
2.2.1 开启方式
- 客户端
在初始化Bugly的时候,添加如下代码
BuglyBuilder builder = new BuglyBuilder(appID, appKey);
build.addMonitor(BuglyMonitorName.MEMORY_JAVA_LEAK);
- 配置项
同时也需要再后台设置Java内存泄漏监控功能的采样率,用户可以通过 Java内存泄漏 配置来调节用户采样率和事件采样率
- sample_ration: Java内存泄漏检测功能的用户采样率,0 所有用户关闭该功能, 1 所有用户开启该功能;
- event_sample_ratio: Java内存泄漏检测功能的事件采样率, 0 所有内存泄漏的事件都不上报, 1 所有内存泄漏的事件都上报;
2.2.2 接口说明
开启Java内存泄漏监控功能后,Bugly会自动对Activity、Fragment这两类对象进行监控:
- 对于Activity:通过注册ActivityLifecycleCallback,收集执行了onDestroy回调的Activity对象;
- 对于Fragment:通过注册FragmentManager.FragmentLifecycleCallbacks,收集执行了onDestroy回调的Fragment对象;
最后对于收集到的这些已经destroyed的对象,会在一个监控线程中进行检测,如果检测多次后,该对象仍未被回收,则会被判定该对象发生了内存泄漏, 除了Activity和Fragment这两类对象外,还可以通过Bugly如下接口来监控任意对象:
Bugly.startInspectLeakObj(leakObj);
/**
* 设置Java内存泄漏待检测对象
*
* @param leakObj 待检测对象
*/
public static void startInspectLeakObj(Object leakObj) {
......
}
2.2.3 日志说明
为了能在同一份日志里面监控多个泄漏对象,Bugly会在检测到一个对象泄漏之后,会再delay一段时间之后才会去抓取hprof文件,这样就可以在同一份日志里面可以检测多个泄漏对象,dump hprof文件的时候,如果有开启Bugly的日志,会有如下Log输出:
adb logcat -s RMonitor_MemoryLeak_LeakInspector RMonitor_Heap_MemoryDumpHelper
检测对象是否泄漏的日志:
06-08 21:03:03.700 25492 25535 D RMonitor_MemoryLeak_LeakInspector: Inspecting com.example.test.memory.TestActivityLeak@73988249-e8b3-4f5b-a880-01f41cb7dc3c Time=1686229383700 count=1
06-08 21:03:04.032 25492 25535 D RMonitor_MemoryLeak_LeakInspector: Inspecting com.example.test.memory.
......
06-08 21:04:33.796 25492 25535 D RMonitor_MemoryLeak_LeakInspector: Inspecting com.example.test.memory.TestActivityLeak@73988249-e8b3-4f5b-a880-01f41cb7dc3c Time=1686229473796 count=19
06-08 21:04:38.968 25492 25535 D RMonitor_MemoryLeak_LeakInspector: Inspecting com.example.test.memory.TestActivityLeak@73988249-e8b3-4f5b-a880-01f41cb7dc3c Time=1686229478968 count=20
06-08 21:04:44.138 25492 25535 D RMonitor_MemoryLeak_LeakInspector: Inspecting com.example.test.memory.TestActivityLeak@73988249-e8b3-4f5b-a880-01f41cb7dc3c Time=1686229484138 count=21
DUMP HPROF文件时候的日志:
06-08 21:07:31.882 25492 29639 D RMonitor_Heap_MemoryDumpHelper: ReportLog dumpHprof: com.example.test.memory.LeftFragment@51fa088b-b1f2-464c-8d70-972a1f26c360
06-08 21:07:33.791 25492 29639 D RMonitor_Heap_MemoryDumpHelper: dump used 1885 ms
06-08 21:07:35.304 25492 29639 D RMonitor_Heap_MemoryDumpHelper: leakFlag=true,ZipFile=true,leakName=com.example.test.memory.LeftFragment@51fa088b-b1f2-464c-8d70-972a1f26c360,dumpPath=/storage/emulated/0/Android/data/com.example.sdkapp/files/Tencent/RMonitor/main/Log/dump_com.example.test.memory.LeftFragment@51fa088b-b1f2-464c-8d70-972a1f26c360_leak_23-06-08_21.07.33.zip
抓取的日志文件后,如果是在WiFi环境,则会立即上传,其他情况需要等进程下一次启动之后才会上传,上传时候的日志为:
06-09 15:58:39.618 23814 23875 I RMonitor_report_File: url: ******* sub_type: activity_leak
2.2.4 Web功能介绍
Java内存泄漏的Web页面主要包括问题列表和问题详情两个页面:
2.2.4.1 问题列表
- 通过内存泄露引用链来作为特征,聚合相同特征的个例,形成Java内存泄露的问题列表。
- 问题列表支持丰富的搜索条件,两组条件的对比分析,帮助用户聚焦严重的泄露问题,或者最近新增的泄露问题。
2.2.4.2 问题详情
- 问题详情,清晰展示泄露引用链,支持用户下载泄露时的内存快照进行详细分析。
2.3 FD详情
Android中一个进程可以使用的FD数量是有限制的,一旦超过最大值,该进程将无法再分配FD资源。如果一个FD打开后没有及时关闭,就会导致FD泄漏,而FD泄漏会导致很多的问题:闪退、黑屏、卡死等问题,所以Bugly建设了一个能力,监控一个进程的FD数量变化,如果达到某一个预设的阈值,会将当前时刻的fd详细信息已经每个fd的分配函数调用栈信息上报给后台。
2.3.1 开启方式
- 客户端
在初始化Bugly的时候,添加如下代码
BuglyBuilder builder = new BuglyBuilder(appID, appKey);
build.addMonitor(BuglyMonitorName.FD_ANALYZE);
- 配置项
- sample_ration: 被采样的用户才会开启FD详情监控功能,0 所有用户关闭该功能, 1 所有用户开启该功能;
- event_sample_ratio: 被采样的设备发生FD触顶之后,上报的概率, 0 所有FD触顶的事件都不上报, 1 所有FD触顶的事件都上报;
- threshold 设置FD触顶的阈值,例如设置为800,那么当FD个数为800的时候会上报日志;
2.3.2 接口说明
暂无
2.3.3 日志说明
1、FD触顶监控功能开启成功的日志
05-16 16:33:38.737 19194 11716 I RMonitor_FdLeak_Monitor: fd leak monitor started.
2、检测到FD触顶时刻的日志
05-16 16:33:48.815 19194 11716 I RMonitor_FdLeak_Trigger: top fd: FdStatisticItem{type=8, count=1654}
3、dump 上报信息的日志
05-16 16:33:48.863 12359 12359 I .example.sdkapp: hprof: heap dump "/storage/emulated/0/Android/data/com.example.sdkapp/files/Tencent/RMonitor/main/fd_leak/dumpN/A_root/heap.hprof" starting...
4、上报日志(WiFi情况下立即上报,其他情况进程重启后上报)
05-16 16:37:17.697 19195 13956 I RMonitor_report_File: url: https://rmonitor.qq.com/v1/*********/upload-file?timestamp=1715848637695&nonce=44ef8384331c486250d430b78a29a18a, sub_type: fd_leak
2.3.4 Web功能说明
2.4 Native内存详情
Native内存使用监控一直是Android平台上面的难点,Google自己也出了很多工具,但基本都是基于本地调试的,并且很多工具使用的时候都会遇到很多问题,例如兼容性、闪退等等,Bugly的Native内存触顶是拦截进程的内存分配和释放情况,然后抓取内存分配的函数调用栈信息,然后在进程的内存(Pss或者Vss)达到一定阈值的时候,会将这些还没有释放的内存分配和函数调用栈信息上报给后台,用户可以通过Bugly的Web页面来检索和分析这些内存触顶的问题;
2.4.1 开启方式
- 客户端
在初始化Bugly的时候,添加如下代码
BuglyBuilder builder = new BuglyBuilder(appID, appKey);
build.addMonitor(BuglyMonitorName.NATIVE_MEMORY_ANALYZE);
- 配置项
- sample_ratio,设备采样率,表示允许多少比例的设备开启相关监控项。
- event_sample_ratio,事件采样率,用来控制,当发生内存触顶时(VSS或者PSS触顶),dump内存进行数据上报的采样率。
2.4.2 接口说明
暂无
2.4.3 日志说明
暂无
2.4.4 Web功能说明
2.5 大图监控
所谓大图,也叫图片过度解码,是指解码后的Bitmap的宽高大于承载其的View宽高的情况, 大图监控通过注册ActivityLifecycleCallback,当DecorView发生onGlobalLayout的时候,遍历DecorView树的所有View,检查每个View的背景图或者src图对应的Bitmap的宽高是否大于View的实际宽高,如果是的话,判定该Bitmap是大图(过度解码)。
2.5.1 开启方式
- 客户端
在初始化Bugly的时候,添加如下代码
BuglyBuilder builder = new BuglyBuilder(appID, appKey);
build.addMonitor(BuglyMonitorName.MEMORY_BIG_BITMAP);
- 配置项
sample_ratio: 大图监控功能的用户采样率, 0 所有用户关闭该功能, 1 所有用户开启该功能;
threshold: 大图判定阈值,150就代表150%,当 (Bitmap宽度/视图宽度>阈值) 或者 (Bitmap高度/视图高度>阈值)时,判定该Bitmap是大图;
2.5.2 接口说明
暂无
2.5.3 日志说明
Bugly检测到大图之后,会尝试立即上报,上报的时候一般会打印如下的日志
06-09 17:00:23.000 31628 31822 D RMonitor_report_Json: url: ***** eventName: BigBitmap, client_identify: ********
2.5.4 Web功能说明
问题列表
- 通过提取过度解码图片的View的布局层次来作为特征聚合问题;
- 支持图片超标大小和图片超标比例排序,快速定位解码超标严重的图片;
问题详情
- 用户可以根据场景信息,以及展示View的布局层次来找到对应的业务场景。
图片大小: 422 * 482,是指解码之后的图片的大小,以像素值为单位;
View大小: 是指承载该图片的View的大小,以像素值为单位;
图片超标比例 = ((422 482) / (300 300)) - 1 = 126.00% , 也就是图片的像素占比View大小的比例值;
图片超标大小 = ((422 482 4) - (300 300 4)) / 1024 = (813616 - 360000) / 1024 = 442.98 KB.
视图类型: 这个字段有两个值,background代表是调用View.getBackground获取的背景图片资源, source代表是通过调用View的getDrawable获取的解码图片资源;
页面: 从上面的截图来看,这个case出现的页面是在 com.example.memory.TestBitmapActivity,页面也就是图片超标的View所在的Activity,这个是不可以自定义的;
场景: 还有一个场景字段,场景字段默认也是当前的Activity,但是该字段的值,业务是可以自定义的, 通过 Bugly.enterScene 和 Bugly.exitScene 来自定义当前的场景,例如当前Activity如果有多个Fragment的切换,那么切换Fragment之后,可以通过场景字段来定位切换到了哪个Fragment。
2.5.5 示例解析
还是以上面问题详情这个问题为示例,从页面这个字段,我们可以看到发生图片超标的页面为 “com.example.memory.TestBitmapActivity”, 另外页面这个字段还可以搭配场景字段来使用,这样可以更加精准的定位问题出现时候的上下文信息。
然后从从堆栈详情这里可以看到过度解码的是一个AppCompatImageView,并且可以看出View的层级关系
com.android.internal.policy.DecorView
android.widget.LinearLayout[0] // 这里的0代表的是DecorView的第一个子View
android.widget.FrameLayout[1] // 类似的,这里的1说明的是LinearLayout的第二个子View
android.support.v7.widget.FitWindowsLinearLayout[0]
android.support.v7.widget.ContentFrameLayout[1]
android.widget.ScrollView[0]
android.widget.LinearLayout[0]
android.support.v7.widget.AppCompatImageView[4] // 最终大图所在的View为android.widget.LinearLayout的第四个子View
它是LinearLayout的第四个子View,那么我们就可以通过Android Studio的 Layout Inspector 等工具来定位到这个View,如下图所示: