Android meminfo字段详解

对于Android内存的统计拆分,需要从系统到进程,再细分到内存分类。Android的内存通常被多个进程 共享,一个进程占用的内存涉及到虚拟内存、物理内存、私有、共享、图形显存等等。想要详细统计一 个进程的内存占用,就需要了解Android的内存管理机制和对内存的分类拆解。

一、Android内存管理

Android内存的管理核心是paging和memory-mapping(mmap).

1.paging

Andoid系统中使用虚拟内存地址来索引内存,虚拟内存被划分为固定大小的page页,典型的页大小为 4K。内存分配最开始都是在虚拟内存上分配,当需要访问这段内存的时候,如果发现它没有存在于物理 内存上(即MMU不能找到这个虚地址va对应的物理地址pa),即发生了缺页(page fault).,缺页有几种 可能:

1.Bug,程序访问了它不应该访问的虚地址空间,android系统会触发访问不合法,ki掉进程。 2.Va是合法的,但是这块va对应的pa还从来没有被分配出来过(例如你mmap的一段内存空间,但是从 来没用过,这时第一次在这块内存上写入),这叫做lazy-allocation,这时系统会真正分配一段物理内存 给你用,然后在页表上对应好这段pa和va。注意第一次写入这里才算真正占用了物理内存,mmap的分 配并不算。 3.Va是合法的,但是这va对应的pa内容当前并没有在物理内存上,而是被swap到一个backup的file上, 这时系统会给这个page在pa上分配物理内存,然后将这块内容从文件读回到物理内存上(swap-in)。

2.swap zram

典型的iux系统的虚拟内存都有swap操作,即一段物理内存在一段时间不用的时候,为了节省物理内存 将他们备份到它的外ackup file.上,一段时间后缺页时再换回。 但是在android上大多数情况是没有这套swap机制的,因为对于移动端的1O代价太大,所以大多数情况 被映射到pa的page是不能被swap的。只有一种情况除外,即如果这段虚拟地址段具有backup file,并且 当他被swap-in到pa后是只读的,那么它是有机会被swap-out回disk的,因为swap这种内存的代价很 小,他们不会在物理内存上被更改,通常这类情况包括那些代码文件的mmap(如dex、so等)。 此外android.上还使用了一种特殊的ZRAM机制来压缩一些物理内存上的page,但是并不把他们swap-out 到disk上,而是仍然在ram中,这是一段被压缩了的page,系统会选择压缩一些page存储在内存中以腾 出一些物理内存的占用。

3.mmap

Linux.上一个重要的特性是memory mapping。如上面所述,va和pa之间通过mmu建立了一个对应关系, 通过page fault来触发pa的分配,此外va还会对应一个backup file,作为swap-in/out时的备份存储。这个 va和backup file之间的对应关系就叫做nemory mapping,我们可以利用这个机制做文件读取。 第一种是映射文件到内存做读取,这时提供一个文件句柄,然后将文件内容映射到一段虚拟内存地址空 间,这样就做到了通过访问虚拟地址空间-造成缺页一swapin backup file的方式来根据需要读取文件。 他相比传统的文件读取效率更高。 第二种是创建一个匿名映射,匿名映射没有backupfile,只是单纯分配一块虚拟内存空间,这其实是 android.上调用new一个对象做的事情,我们new一块int的数组,事实上在new后很可能是通过mmap分 配了一块虚拟内存空间,而只有当第一次写入的时候才触发缺页而占用真正的物理内存,所以统计 android的物理内存占用不是看new了多少,或者文件映射了多少,而是实际上虚拟内存缺页了多少。 本成224

二、meminfo数据详解

通过命令dumpsys meminfo package._name|pid,查看指定进程的内存使用情况。通过输出的信 息,可以看出来应用在内存哪里分配出现了问题,比如native heap分配高,就要查一下自己的native部 分的代码哪里分配后没有及时释放。 dumpsys位于/frameworks/native/cmds/dumpsys。运行时它首先后获得ServiceManager,然后 根据参数查找服务,找到后binderi调用服务的dump方法。meminfo位于/system/memory/libmeminfo,是 系统的一个服务(内部则是一个MemBinder的类实现的),当dumpsys找到这个服务后,调用dump方法,通 过传递的参数,显示出进程内存使用情况。

下面是system_serveri进程的meminfo,

这里面可以看到oss privatedirty privateclean swapped diry这几个重要的指标。

pss
Proportional Set Size
进程实际占用的物理内存大小,但是android内存涉及到在系统中同其他进程共享一部分库(可能是so,可能是字体文件等的mmp),所以这里面会考虑这个因素计算你的进程平摊的这部分共享库的大小,这里的proportional就意味着统计了这个平摊后的物理内存。这是衡量进程占用内存的最真实指标。
rss
Resident Set Size
常驻内存大小,是单个进程实际占用的内存大小;RSS易被误导的原因在于,它包括了该进程所使用的所有共享库的全部内存大小。对于单个共享库,尽管无论多少个 进程使用,实际该共享库只会被装入内存一次。对于单个进程的内存使用大小, RSS不是一个精确的描述。
Rss=Shared Clean+Shared Dirty+Private Clean+Private_Dirty
uss
Unique Set Size
进程独占的内存
Vss
Virtual Set Size
VSS=RSS+未分配实际物理内存
内存的大小关系:VSS>=RSS>=PSS>=USS
swappablePss
能够被共享的内存。
sharing_proportion =(pss-uss)/(shared_clean shared_dirty)
swapable_pss=(sharing_proportion shared_clean)+private_clean
privateDirty
privateClean
它们指的是每个进程独有的内存页面。PrivateDirty指的是进程独有的脏页面,即进程
已经修改但尚未提交到磁盘的页面;PrivateClean指的是进程独有的干净页面,即进程已经提交到磁盘的页面。
sharedDirty
sharedClean
指的是进程和别的进程共享的内存页面。sharedDirty是该过程和至少一个其他过程所
引用的映射中的页面,并且由这些过程中的至少一个过程写入的页面;sharedClean
是指该进程和至少一个其他进程已引用但未由任何进程修改的映射中的页面
Swapped Dirty
指的并不是被swap-out出去的内存,而是android?系统中zram机制压缩掉的部分
Private Dirty部分的不常用物理内存,这个重要度等同于Private Dirty,因为哪些
Private Dirty会被压缩不能被控制,所以这部分多通常也是Private Dirty的。

因为PSS中已经包含了Private Dirtyi和Private Clean,但是没包含swapped dity,所以最终衡量你的 进程对物理内存的占用应该是取PSS+Swapped Dirty 这里统计的所有值都是物理内存大小,即缺页的那些虚拟内存的大小,而不是真正虚拟内存的大小, 注意这里看到的所有内存大小都是真真实实占着你的物理内存的。比如,new1个int[1024],在对这 块内存写入之前它都不会占用物理内存并体现在这里。 1.数据来源 根据属性的不同,进程的虚拟地址空间被划分成若干个vma,每一个vma通过vm_next和vm_prev组 成双向链表,链表头位于进程的taskmmmmap。通过cat proc//maps可以得到该进 程的maps数据如下。每一行的vma对应着起始地址、结束地址、属性、偏移地址、主从设备号、 inode编号和文件名。当通过proc接口读取进程的smaps.文件时,内核会首先找到该进程的vma链表 头,遍历链表中的每一个vma,通过walk_page_vma统计这块vma的使用情况,最后显示出来。

smaps文件是基于maps的扩展,它展示了一个进程的内存消耗,比同一目录下的maps文件更为详细。 如下是maps中一条vma的详细数据,全部数据同样可通过cat/proc//smaps获取。

7fce3a5000-7fceba4000  rw-p   00000000 00:00 0
Size:                  8188  kB
KernelPagesize:           4  kB
MMUPagesize:              4  kB
Rss:                    156  kB
Pss:                    152  kB
Shared Clean:             0  kB
Shared_Dirty:             4  kB
Private Clean:            0  kB
Private_Dirty:          152  kB
Referenced:             156  kB
Anonymous:              156  kB
LazyFree:                 0  kB
AnonHugePages:            0  kB
ShmemPmdMapped:           0  kB
FilePmdMapped:            0  kB
Shared_Hugetlb:           0  kB
Private_Hugetlb:          0  kB
Swap:                     0  kB
SwapPss:                  0  kB
Locked:                   0  kB
THPeligible:              0
VmFlags:rd wr mr mw me gd ac

AMS中dump meminfo功能最终也是通过JNl调用libmeminfo读取进程smaps的节点。

在ProcessRecord中查找。

@GuardedBy(anyof {"mService","mProcLock"})
ArrayList<ProcessRecord>collectProcessesLOSP(int start,boolean allPkgs,
String[]args){
	ArrayList<ProcessRecord>procs;
	if(args!=null&args.length>start//dump指定进程
		&args[start].charAt(0)!=-'){
		procs new ArrayList<ProcessRecord>();
		int pid =-1;
 
        try{
            pid = Integer.parseInt(args[start]);
        } catch (NumberFormatException e){
 		} 
 
        for (int i mLruProcesses.size()-1; i >0; i--){
            ProcessRecord proc mLruProcesses.get(i);
            if (proc.getpid()>0 &proc.getpid()==pid){
            	procs.add(proc);
        	} else if (allPkgs &proc.getpkgList()!=null
       		 		&& proc.getPkgList().containsKey(args[start])){
        		procs.add(proc);
        	} else if (proc.processName.equals(args[start])){
        		procs.add(proc);
          	}
        if (procs.size()<=0){
        	return null;
        }
 
	} else[  //dump所有进程
		procs = new ArrayList<ProcessRecord>(mLruProcesses);
 
	return procs;
}	

Java进程会额外dump一些Object、SQL、DATABASES:等信息

pw.println("objects");
printRow(pw, TWO_COUNT_COLUMNS, "Views:"viewInstancecount,"ViewRootImpl:"
viewRootInstanceCount);
 
printRow(pw,TWO_COUNT_COLUMNS,"AppContexts:"appContextInstanceCount,
"Activities:"activityInstanceCount);
 
printRow(pw,TWO_COUNT_COLUMNS,"Assets:"globalAssetcount,
"AssetManagers:"globalAssetManagerCount);
 
printRow(pw,TWO_COUNT_COLUMNS,"Local Binders:"binderLocalobjectcount,
"Proxy Binders:"binderProxyobjectCount);
 
printRow(pw,TWO_COUNT_COLUMNS,"Parcel memory:"parcelsize/1024,
"Parcel count:"parcelCount);
 
printRow(pw,TWO_COUNT_COLUMNS,"Death Recipients:"binderDeathobjectCount,
"OpenSSL Sockets:"openSslSocketCount);
 
printRow(pw,ONE_COUNT_COLUMN,"Webviews:"webviewInstanceCount);
 
// SQLite mem info
pw.println("")
pw.println("SQL");
printRow(pw,ONE_COUNT_COLUMN,"MEMORY_USED:"stats.memoryUsed 1024);
printRow(pw,TWO_COUNT_COLUMNS,"PAGECACHE_OVERFLOW:"
stats.pageCacheoverflow / 1024,"MALLOC_SIZE:"stats.largestMemAlloc / 1024);

2.内存分类 Native Heap: 这是C/C++层直接通过malloc分配的内存 Dalvik Heap/other: 这是android的java虚拟机分配的内存,也就是java部分分配的, Stack: 栈分配内存 Ashmem: 进程的匿名共享内存Anonymous Shared Memory GFX dev: 通俗来说是显存,Android显存和内存在同样的物理设备上,所以统计的总内存是包括显存 的,至于adb如何知道哪些是显存,是因为gles和egl的so库在分配显存的时候也是使用的带backup文件 的mmap,adb只是简单的统计了所有gles和egl的mmap将其视为显存,这部分通常就是在gpu上的资 源,gpu上的资源由很多种,占比较多的就是texture,buffer,shader programe,通常是游戏应用。 Other dev:除显卡外所有其他硬件设备的mmap后的物理内存,可能包括声卡等,通常不多。

.so mmap: 这个就是so库本身文件mmap占用的物理内存,随着程序运行会逐渐的读取so文件,造成和 缺页的部分就是在物理内存产生占用,这部分大就是so库太大了,但是这部分因为有很多是readonly的 mmap,所以有更大的机会被swap-out出去。 apk.dex oat.art mmap:这些都是android程序文件本身被mmap占用的内存,和so的性质差不多。 Other mmap:是所有除了上面的之外其他的所有非匿名方式的mmap,想要知道是什么可以通过下面 要讲的命令查看。 Unkonwn: 在meminf中它指除了部分已知的有名字的匿名映射(以anon:为前缀,例如”[anon:dalvik-”) 之外的所有的匿名mmap,还有其他的未命名的vma(name字段不显示),因为无法分辨来源,就统计 在unkown中。例如,看到anon:dalvik-*,那它一般是dalvik虚拟机的分配,这个最终会被统计到 meminfo的dalvik里。

anon: 这个就是匿名mmap映射,除了部分已知的匿名映射如”[anon:dalvik-”,” [anon:stack and tls:“等,其他匿名映射所占用的内存最终会被统计到neminfo的unkown里。 anon:libc_malloc这个是通过malloc方法进入的mmap,即所有的new,malloc调用。 Kgsl-3d:则是gles对显存硬件的虚拟内存映射,换句话说就是显存,meminf正是统计这个标签来获取 gfx的大小o

更详细的拆解在android_os_Debug.cpp中。如下表所示,是对smaps?字段的详尽匹配规则。

子类值标签值文件名
HEAP_NATIVE”Native Heap”[heap] [anon:libc_malloc][anon:scudo:[anon:GWP-ASan
HEAP_STACK”Stack”[stack [anon:stack_and_tls:
HEAP SO”.so mmap”.so
HEAP JAR”.jar mmap”.jar
HEAP APK”.apk mmap”.apk
HEAP TTF”.ttf mmap”.ttf
HEAP DEX”.dex mmap”
HEAP_DEX_APP_DEX”.App dex”.odex .dex
HEAP_DEX_BOOT_VDEX”.Boot dex”.vdex &&@boot or /boot or lapex
HEAP_DEX_APP_VDEX”.App vdex”.vdex
HEAP_OAT”.oat map”.oat
HEAP ART”.art map”.art .art]
HEAP ART BOOT”.App art”system@framework@bootand system/framework/boot apex
HEAP ART APP”.App art”
HEAP GL DEV”Gfx dev”/dev/kgsl-3do
HEAP CURSOR”Cursor”/dev/ashmem/CursorWindow
HEAP SHMEM”Ashmem”/dev/ashmem
HEAP_DALVIK_OTHERHEAP_DALVIK_OTHER_APP_CODE_CACHE”.AppJIT”/memfd:jit-cache
HEAP DALVIK_OTHER ZYGOTE_CODE CACHE”.ZygoteJIT”/memfd:jit-zygote-cache
/dev/ashmem/jit-zygote-cache
HEAP_DALVIK_OTHER”Dalvik Other”[anon:dalvik-
HEAP_DALVIK_OTHER_LINEARALLOC”.LinearAlloc”[anon:dalvik-LinearAlloc
HEAP_DALVIK_OTHER_COMPILER_METADATA”.CompilerMetadata”[anon:dalvik-CompilerMetadata
HEAP_DALVIK_OTHER_APP_CODE_CACHE”.AppJIT”[anon:dalvik-jit-code-cache or [anon:dalvik-data-code-cache
HEAP_DALVIK_OTHER_INDIRECT_REFERENCE_TABLE”.IndirectRef”[anon:dalvik-indirect ref