Android 性能测试 - 内存
1、内存了解
在Android App的性能优化的各个部分里,内存方面的知识较多且不易理解,内存的问题绝对是最令人头疼的一部分,需要对内存基础知识、内存分配、内存管理机制等非常熟悉,才能排查题。
1.1 了解进程的地址空间
在32位操作系统中,进程的地址空间为0到4GB,这里主要说明一下Stack和Heap:
Stack空间(进栈和出栈):
由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小。
Heap空间:
进程地址空间-引用自CSDN它的使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。
1.2 Android的内存管理
Android系统的ART和Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色, 使用paging 和 memory-mapping来管理内存,这意味着不管是因为创建对象还是使用使用内存页面造成的任何被修改的内存,都会一直存在于内存中,App唯一释放内存的方法就是释放App持有的对象引用,使GC可以回收。
1.2.1 Android的应用进程按共享/私有分类如下:
首先来了解什么是共享内存
Android系统的ART和Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色, 使用paging 和 memory-mapping来管理内存,这意味着不管是因为创建对象还是使用使用内存页面造成的任何被修改的内存,都会一直存在于内存中,App唯一释放内存的方法就是释放App持有的对象引用,使GC可以回收。
1.2.1 Android的应用进程按共享/私有分类如下:
首先来了解什么是共享内存
Android应用的进程都是从一个叫做Zygote的进程fork出来的。Zygote进程在系统启动并且载入通用的framework的代码与资源之后开始启动。为了启动一个新的程序进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这使得大多数的RAM pages被用来分配给framework的代码,同时使得RAM资源能够在应用的所有进程之间进行共享。
image
大多数static的数据被mmapped到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code,app resources,so文件等。
大多数情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间进行共享的机制。例如,Window Surface在App与Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider与Clients之间共享内存。
共享内存:Dalvik虚拟机代码、应用框架的代码、应用框架的资源应用框架的SO库。
私有内存:应用的代码、应用的资源、应用的SO库
共享/私有内存:堆内存,其他部分
1.2.2 android进程中内存分类如下:
android进程中内存分类native heap:是lib层C/C++库所占用的内存(Native代码分配的内存,虚拟机和Android框架本身也会分配),不包含dalvik实例的linux进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。
Dalvik heap:Dalvik虚拟机使用的内存,包含dalvik-heap和dalvik-zygote,堆内存,是java实例对象的空间以上两个heap空间完全由程序员控制,是最主要的两块内存,另外还有下面3种:
Dalvik Other:类的数据结构和索引
so mmap:Native代码和常量
dex mmap:Java代码和常量
1.2.3 查看内存占用
通过命令行adb shell dumpsys meminfo packagename查看内存详细占用情况。
其中几个关键的数据:
-- Private(Clean和Dirty的):应用进程单独使用的内存,代表着系统杀死你的进程后可以实际回收的内存总量。通常需要特别关注其中更为昂贵的dirty部分,它不仅只被你的进程使用而且会持续占用内存而不能被从内存中置换出存储。申请的全部Dalvik和本地heap内存都是Dirty的,和Zygote共享的Dalvik和本地heap内存也都是Dirty的。
-- Dalvik Heap:Dalvik虚拟机使用的内存,包含dalvik-heap和dalvik-zygote,堆内存,所有的Java对象实例都放在这里。
-- Heap Alloc:累加了Dalvik和Native的heap。
-- PSS:这是加入与其他进程共享的分页内存后你的应用占用的内存量,你的进程单独使用的全部内存也会加入这个值里,多进程共享的内存按照共享比例添加到PSS值中。如一个内存分页被两个进程共享,每个进程的PSS值会包括此内存分页大小的一半在内。
-- Dalvik Pss内存 = 私有内存Private Dirty + (共享内存Shared Dirty / 共享进程数)
-- TOTAL:上面全部条目的累加值,全局的展示了你的进程占用的内存情况。
-- ViewRootImpl:应用进程里的活动窗口视图个数,可以用来监测对话框或者其他窗口的内存泄露。
-- AppContexts及Activities:应用进程里Context和Activity的对象个数,可以用来监测Activity的内存泄露。
1.3 内存回收
在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的gc操作速度会比Old Generation区域的gc操作速度更快。
2、内存测试方法
基于上面的理论学习,可以知道内存问题基本上就是三种:内存抖动、内存泄漏、内存溢出。我们测试内存的时候也主要关注这三个测试点。至于用什么方法进行测试,下面简单列举一下工具,基本上网上都有关于工具使用很详细的教程,在此不再详细述说。
2.1 检测内存抖动
内存抖动:大量的对象被创建又在短时间内马上被释放。
瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
Memory Monitor:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号(用于发现有没有内存泄漏和严重内存抖动)。
存在内存抖动
2.2 检测内存泄漏
内存泄露可以引发很多的问题:
1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)
2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。反之内存占用越小,在后台存在的时间就越长)
3.直接崩溃(OutOfMemoryError)
内存泄漏无疑会严重影响用户体验,一些本应该废弃的资源和对象无法被释放,导致手机内存的浪费,app使用的卡顿,那么如何排查内存泄漏呢?
一个terminal指令:
adb shell dumpsys meminfo (package name)
这条指令是用来查询这个进程所占用的内存的具体详情的,通过这条指令可以看到当前app在手机中占用的具体的堆内存大小,view的数量,activity的数量等等。
meminfo_内存泄漏
其中activity数目是非常关键的一个信息,可以帮助我们快速地检测出内存泄漏。我们可以反复地进入退出需要测试的目标activity,如果在反复进入退出之后,用terminal执行上面的语句查询当前的内存情况,如果发现activity数量一直在增长,如上图所示,APP退出后,再进入相当界面时Views和activity数量成倍地增长,则很大可能存在内存泄漏。
另外以下4个是用于定位的内存抖动和内存泄漏发生的具体位置:
- Allocation Tracker:
使用此工具来追踪内存的分配.
但是事实上,通过观察这个内存曲线的增长来或者是观察allocate tracker中的allocate data数值的增长来检测是否有内存泄漏问题,不太靠谱,因为往往内存泄漏发生了,但是GC仍然可以通过回收其他对象的方式腾出空间,导致这个数据的变化基本看不出来,甚至是减小的。
- Heap Tool:
查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的。
- Memory monitor :
如果是Dalvik内存泄漏,也可以使用Android Device Monitor dump出一份hprof文件(别忘了先手工Cause GC),生成hprof文件进行测试分析。用hprof分析工具,可以检测到泄漏的activities、分析出重复定义的字符串。
这里能够实时地显示应用程序占用的内存,很方便我们查看。总的来说,就是使用monitor memory功能监测app主进程占用的内存,触发GC操作,而后观察内存的占用情况,如果在使用的过程中内存不断增加,没有回落,很有可能发生了内存泄漏,这时候就需要对生成的HPROF文件进行深入分析了。
hprof使用HPROF文件分析工具标准步骤如下:
(1)打开Captures窗口,双击你想要查看的HPROF文件,打开HPROF文件查看工具界面;
(2)点击Android Studio主窗口右边栏上的Analyzer Tasks,默认HPROF文件分析工具会出现在HPROF文件查看工具的右边。Analyzer Tasks列表中选择你想分析的选项;
(3)点击开始分析的按钮;
(4)查看分析结果,点击结果中条目可在HPROF文件分析工具中查看详情。一般查看Retained Size占用最大的类,分析是否有内存泄漏。
附录:
-
Class name
类名 -
Total Count
该类的实例总数 -
Heap Count
所选择的堆中该类的实例的数量 -
Sizeof
单个实例所占空间大小(如果每个实例所占空间大小不一样则显示0) -
Shallow Size
堆里所有实例大小总和(Heap Count * Sizeof) -
Retained Size
当该对象被GC回收时,所释放掉的内存大小 -
Instance
具体的实例 -
Reference Tree
所选实例的引用,以及指向该引用的引用。 -
Depth
GC根节点到所选实例的最短路径的深度 -
Shallow Size
所选实例的大小 -
Dominating Size
所选实例所支配的内存大小
MAT
上述只是可以粗略的看出是不是有问题,而要知道问题出在哪里就需要借助MAT了。将生成的.hprof文件进行转换,然后使用MAT打开来分析应用的内存使用情况。通常在使用MAT打开hprof文件后,能够在首页看到Top Comnsumers和 component Report等功能,我们可以快速定位一些大块的内存消耗。
但我们在分析时会发现系统资源类占据了很大一部分内存,因此为去除这部分对分析的干扰,我们在使用AndroidSDK提供的hprof-conv转换时需要增加一个参数:
hporf- conv [-z] <infile><outfile> -z:exclude non-app heaps,such as Zygote
如果hprof文件是已经转换过的,则可以使用OQL:
//在数据中寻找应用的Application类对象,将对象地址转换为十进制后输入以下查询语句:
select * from instanceof java.langObject s where s.@objectAddress> 1107296256
//(后面那串数字应该是Application类对象的地址)
采用这两种方法后,再使用MAT来分析就可以比较容易发现自身代码的内存问题。
MAT 是探索 Java 堆并发现问题和好帮手,能够迅速发现常见的图片和大数组等问题;
内存碎片问题一般隐藏在对象的地址中;
如需要测试非 Dalvik部分,有必要了解 Linux 的进程和内存原理、内存共享机制,熟悉常用命令行工具;
内存分配的最小单位是页面,通常为4KB,这个限制会引发各种问题;
2.3 检测OOM
Android系统的每个进程都有一个最大内存限制(这个阈值可以是48M、24M、16M等,视机型而定),如果申请的内存资源超过这个限制,系统就会抛出OOM错误。
PS:可以通过adb命令查看阈值
adb shell getprop | grep dalvik.vm.heapgrowthlimit
[dalvik.vm.heapgrowthlimit]: [192m]
Android 2.x系统,当dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生OOM。其中bitmap是放于external中 。
Android 4.x系统,废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生OOM(art运行环境的统计规则还是和dalvik保持一致)
内存溢出是程序运行到某一阶段的最终结果,直接原因是剩余的内存不能满足内存的申请,但是再分析间接原因内存为什么没有了:
内存泄漏的存在可能导致可用内存越来越少;
内存申请的峰值超过了系统时间点剩余的内存;(例如:某手机单个进程可用最大内存为192M,目前分配内存80M,此时申请5M内存,但是当前时间点整个系统可用内存只有3M,此时没有超出单个进程可用最大内存,但是OOM也会发生)
2.4 常见内存测试场景
2.4.1 按各部分内存的用途设计场景
(1)比较操作前后或不同版本的内存变化
(2)显示多张图片的前台进程
(3)多个场景来回切换
(4)长时间运行进程的内存增长
2.4.1 根据比较结果,确定问题方向
android进程中内存分类(1)Dalvik Heap内存
持续增长
内存泄露 -> LeakCanary / MAT
频繁GC,大幅度波动
大量的分配和释放 -> Allocation Tracker
比以前版本稳定增长
新功能及代码改动 -> Heap Dump / MAT
Heap Alloc不变,PSS增加
可能存在内存碎片 -> Heap Dump / MAT
(2)非Heap内存
Dalvik Other
类信息
载入Class数正相关
mmaps
可执行代码
常量
3、XX银行APP性能评测-内存测试结果分析
3.1 总览
从内存占用对比看,行业竞品均值为351.3M,90分位约262.6M,75分位约339.5M,中位数约426.4M,25分位约605.7M。【榕商Bank】内存占用均值为246M,表现良好,打败了行业90%以上的竞品,请继续保持哦。
image3.2 启动首页加载内存问题分析--存在内存抖动
这里选取了启动加载场景来进行内存问题的分析。
实际上从上面的总览数据分析看内存占用不同场景对比很难发现内存问题,但是同一个场景内存占用曲线图是可以发现问题的,如果曲线图有锯齿形的抖动且持续上升,基本上可能存在内存问题,如下图可得出首页存在内存抖动问题
首页加载内存抖动图1 首页加载内存抖动图2另外还需要以日志辅助分析内存问题:从日志上看,1秒甚至几百ms内就有一次GC,而且是主动GC,说明在频繁申请内存,总阻塞耗时约5001 ms
GC GC日志注释:
GC Reason:GC触发原因
GC_CONCURRENT:当已分配内存达到某一值时,触发并发GC;
GC_FOR_MALLOC:当尝试在堆上分配内存不足时触发的GC;系统必须停止应用程序并回收内存;
-GC_HPROF_DUMP_HEAP: 当需要创建HPROF文件来分析堆内存时触发的GC;
-GC_EXPLICIT:当明确的调用GC时,例如调用System.gc()或者通过DDMS工具显式地告诉系统进行GC操作等;
Amount freed GC:回收的内存大小
Heap stats:堆上的空闲内存百分比 (已用内存)/(堆上总内存)
Pause time:这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间。
4、App端内存问题排查思路:
(1)Service停止使用时,是否被销毁
(2) 当界面变为不可见时,是否释放当前界面的资源
(3)内存变少时,是否有释放内存
(4) bitmap使用完之后是否被回收
(5)是否有大量第三方库的消耗