Android内存分析和调优(上)

最近我们的android app占用了大量内存,于是领导安排做减少内存占用的工作。
要优化内存,首先要做的就是分析内存占用情况。android提供了多个工具和命令进行内存分析。
 

第一层 Procrank

 
很粗略的,可以使用"adb shell procrank",结果类似于

PID    Vss        Rss        Pss       Uss      cmdline

......
2319 42068K 42032K 13536K 7028K com.xxx
......

该命令可以列出当前系统所有进程的内存占用情况。
PID是进程ID。
Vss是占用的虚拟内存,如果没有映射实际的内存也算进来。
Rss是占用的物理内存。是共享内存+私有内存。因为共享内存是多个进程共用的,所以存在重复计算。
Pss是占用的私有内存加上平分的共享内存。例如一块1M的共享内存被两个进程共享,那每个进程分500K。各进程的Pss相加基本等于实际被使用的物理内存,所以这个经常是最重要的参数。
Uss是私有内存。
cmdline可以看做是apk包名。

通过procrank,只能很宏观的横向比较不同的应用。如果要更细致的了解具体内存是如何使用,则需要进入

第二层 dumpsys meminfo

命令“adb shell dumpsys meminfo package.name”。在4.0 ICS(或者3.0 HoneyComb)之后的系统上,会看到类似下面的输出

                                 Shared   Private  Heap    Heap     Heap
                      Pss      Dirty      Dirty     Size     Alloc      Free
                      ------   ------    ------     ------  ------     ------
Native            16        8           16        3416   3300     79
Dalvik            3884    10592   3580    9560   9022     538
Cursor            0          0           0 
Ashmem        0           0           0 
Other dev       5110    10244   0 
.so mmap       640     1948      396 
.jar mmap      0          0           0 
.apk mmap     68        0           0 
.ttf mmap       817      0           0 
.dex mmap     411      0           0 
Other mmap   55        16         32 
Unknown        2404     660       2388 
TOTAL            13405  23468   6412    12976 12322 617

(如果使用2.3或之前的版本,结果会粗糙一些,很多都被归入了Other,但基本结构是一样的)

stacktrace上有个经常被搜到的帖子对这个格式有说明,虽然针对的是android 2.3格式,但读后非常有收获。
但仍有很多疑问没有解答,例如针对上面的例子,为什么Native heap size那么大,但Pss却那么小?占用内存比较多的Other dev是什么?Unknown又有哪些?等等。
要理解这些,需要知道这个report是如何生成的。实际上,生成report的代码是android的android_os_Debug.cpp。
从中我们可以发现,上面列表的数据是由三种方式获取的:
1. Pss/Shared Dirty/Private Dirty三列是读取了/proc/process-id/smaps文件获取的。它会对每个虚拟内存块进行解析,然后生成数据。
2. Native Heap Size/Alloc/Free三列是使用C函数mallinfo得到的。
3. Dalvik Heap Size/Alloc/Free并非该cpp文件产生,而是android的Debug类生成。

后面两个Heap的获取比较简单,我唯一的疑惑是为什么有free的?我的理解是无论是c的malloc还是java的new,最后都是通过mmap系统调用进行内存分配的。而mmap必须以页的4K为单位。所以如果一次一次只需要malloc 2K,则剩下的2K是free的。如果下次再malloc 2K,可以仍然使用上次mmap剩余的2K内存。

至于smaps文件,我们可以通过adb shell cat /proc/process-id/smaps来查看(需要root)。这是个普通的linux文件,描述了进程的虚拟内存区域(vm area)的具体信息。每次mmap一般都会生成一个vm area。
在Android上,一个更加方便的命令是adb shell showmap -a process-id。

第三层 adb shell showmap

该命令也是读取smaps文件,但结果细化的具体的vm area。
该命令输出的每行表示一个vm area,列出了该vm area的start addr, end addr, Vss, Rss, Pss, shared clean, shared dirty, private clean, private dirty,object。 
第二层的dumpsys meminfo其实就是读取这些数据,然后分类(native, dalvik, .so map, etc.)统计生成。
start addr和end addr表示进程空间的起止虚拟地址。
Vss,Rss,Pss跟前面说的一样。
Object可以看做mmap的文件名。

Shared clean,按字面意思,表示共享的干净的数据。共享表示多个进程的虚拟地址可以都指向这块物理空间,表示多个进程共享的so库。为什么这里说是多个进程共享的so而不是所有的so呢?
关于so库的加载,我一直觉得是mmap带MAP_SHARED参数,但看了memory_faq,才知道是MAP_PRIVATE。如果使用showmap命令查看vm area,会发现有的so的内存都属于Shared clean,而有的so则属于private clean。前者一般是当前进程特有的so,而后者一般是通用的so。后来看了对mmap的各种参数的实验(很赞实践精神),才知道第一次以MAP_PRIVATE mmap so,内存都是private clean的。如果另外一个进程mmap了同一个so,那该vm area就变成shared clean了。

Private clean,包括该进程私有的干净的内存。包括前面说的该进程独自使用的so和进程的二进制代码段。
Clean内存的好处是在内存紧张时,可以释放物理内存。因为是clean的,所以不需要写回到disk,只需要下次读取该内存(导致缺页错误)时再从disk读入。

Private dirty,表示该进程私有的不跟disk数据一致的内存段。例如堆(heap),栈(stack),bss段。关于bss段,因为在elf文件为了节约控件没有赋值,所以在加载到内存时赋值为0,于是跟disk就不一致了。在showmap结果中,会发现几乎每个so都有一个显示位[bss]的private dirty段。数据段我估计是private clean的,因为elf文件是有初值的。

Shared dirty开始我一直搞不清楚。后来看了Dalvik vm internal这个video(slides),才明白了些。对于普通的linux进程,当父进程fork子进程时,父进程的虚拟内存区域都会”复制“一份到子进程中。这里”复制“加引号,是因为为了节省内存,也为了减少内存拷贝的时间,使用的是copy-on-write的方法。当子进程对private dirty的堆,栈,bss没有修改时,则是父子进程share这份dirty(因为跟disk没法映射)数据。如果发生改变,则会修改为private dirty。所以android有zygote进程,是所有android apps进程的父进程,在其中会加载resource等资源(下文会看到,最简单的应该也有大概5M resource,例如图片),这些资源都是只读的。具体的apps继承了这些shared dirty的数据,因为不修改它们,所以也不用分配多余的内存空间。

由于android使用的linux没有swap分区,所以dirty的数据必须常驻内存。所以dumpsys meminfo会把private dirty和shared dirty重点列出来,这也是我们优化内存的重点。

现在可以回答一个前面提到的问题,为什么Native Heap(根据mallinfo系统调用得到)很大而Native Pss(根据swaps得到)很小。我觉得这是dumpsys meminfo的一个bug。根据android_os_Debug.cpp的代码,object名字是[heap]的段被认为是native heap。这在2.3是正确的,但在4.0之后,[heap]为名字的段却很小(只有几K)。同时,我却发现有大量的[anon]的区域。我认为anon是anonymous的缩写。malloc一般是通过mmap来分配内存的,而参数是MAP_ANONYMOUS。所以我觉得这些[anon]是native heap。从大小上看,现在这些[anon]被看做是Unkown的一部分,也跟hative heap的大小差不多。

在dumpsys meminfo结果的其他值比较大的行,.so表示映射的so库(vm area行的object名称包含.so字样),.dex表示映射的.dex文件(dalvik的虚拟机二进制码),Other dev表示映射其他的/dev的(dalvik的heap也是映射到特殊的/dev上)。加上native和dalvik的heap,下次写如何具体分析这五项。

你可能感兴趣的:(Android内存分析和调优(上))