Skip to content

Android内存(1): app内存浅析

clarkehe edited this page Sep 7, 2016 · 3 revisions

android是基于java的开发,做java开发相对简单,不像C++要关心内存,各种内存问题搞的焦头烂额。但是否真的就完全不用关心内存问题了呢?不行。**android也有一些内存问题,内存泄露、内存溢出(OOM)、内存占用过多,GC回收导致性能下降等。**这里不讲这些具体的问题,主要分析下一个app会用到那些内存,以及这些内存的特点,这些内存在使用上有什么要注意的地方。

一、内存基本概念

虚拟地址空间和物理地址空间
内存是程序运行时的存储地址空间,可分为虚拟地址空间和物理地址空间。虚拟地址空间是相对进程而言的,每个进程都有独立的地址空间(如32位程序都有4GB的虚拟地址空间)。物理地址空间就是由硬件(内存条)提供的存储空间,物理地址空间被所有进程共享。

从程序角度看,我们谈到的地址空间一般是虚拟地址空间,通过malloc或new分配的内存都虚拟地址空间的内存。虚拟地址空间与物理地址空间的都是以page为最小管理单元,page的大小因系统而异,一般都是4KB。虚拟地址空间有到物理地址空间的映射,如果要访问的虚拟地址空间没有映射到物理地址空间,操作系统会产生缺页中断,将虚拟地址空间映射到物理地址空间。

因此,程度的虚拟地址空间比物理的地址空间要大的多。在较多进程同时运行时,物理地址空间有可能不够,操作系统会将一部物理地址空间的内容交换到磁盘,从而腾挪出一部分物理地址空间来。磁盘上的交换区,在linux上叫swap area,windows时叫page file。

android底层基于linux,不过android是没有交换区的(为什么没有?),所以android系统的内存资源就更加宝贵。为更合理、充分利用有限内存资源,android引入一个low-memory-killer机制,在内存不足,根据规则回收一部分低优先级的进程,从而释放他们占有的内存。low-memory-killer机制我们在后面文章中单独再说。

根据官方文档,虽然没有交换区,还是有部分内存是可以被释放的。”That is with one exception: any files mmapped in without modification, such as code, can be paged out of RAM if the system wants to use that memory elsewhere. “ 就是文件映射的内存空间,如果内容没有修改,还是可以从物理内存中被换出来的。

堆和栈
做过c++开发,应该都清楚堆与栈的概念。native的程序中,地址不是堆空间就是栈空间。

堆是需要向系统申请分配的,可指定起始地址(或由系统指定)和空间大小。进程在启动时,系统会创建默认的堆空间,如c运行库中的malloc和new就在默认的堆空间中再分配空间。

也可以调用系统的接口创建新的堆空间,如linux的brk和mmap接口,windows的HeapCreate和CreateFileMapping接口。mmap与CreateFileMapping比较特别,它们是把文件(设备)映射到虚拟地址空间,读写虚拟地址空间就是读写文件的内容;文件可以是真实的文件(设备),也可以是不存在的文件,在linux叫匿名内存,windows叫共享内存。

在进程中,用的最多就是堆空间了,有系统创建的,有程序自己创建的。堆空间比较灵活,由程序自己创建与销毁。进程根据不同的需求创建各种堆空间,这些堆空间可以被访问。后面在分析app的内存时,会有各种用途的内存空间,不要被各种名称所迷惑,它们作本质上都是堆空间。堆空间本质就是由系统分配的一块连续虚拟地址空间。

栈也是一块连续的地址空间,用于存储临时变量或函数调用中传递的参数。栈的空间由程序运行时根据需要进行分配,使用完即释放。通常我们说的函数调用栈,就是这个栈。在函数调用过程中,参数和一些临时变量会保存在栈中(压栈),函数调用越深,占用栈的空间也就越大。在函数从内向外一步一步返回时,栈的空间也不断被释放(出栈)。

Private RAM和Shared RAM
从内存是否包括共享内存的角度,可分为Private RAM和Shared RAM。

Private RAM (Unique Set Size)
私有内存,被app单独占用(没有与其他进程共享)的内存。如果app进程退出的话,系统可以回收这么多内存。

Shared RAM
两个或多个进程共享的地址空间。虽然两个进程的虚拟地址空间是独立的,但都对应同一份物理地址空间。

在android,有一个描述app占用(物理)内存大小概念:按比例分配占用内存(Proportional Set Size),简称PSS。PSS = Private RAM + 按比例计算的Shared RAM。除了Private RAM,PSS还包括与其他进程共享的内存,在计算进程这部分内存大小时,按比例计算。PSS一般是大于Private RAM的。

Clean RAM和Dirty RAM
从内存是否被修改的角度,可分为Clean RAM和Dirty RAM。

Clean RAM
内存在分配时,用磁盘的镜像或资源文件,或者是用0,对内存进行了填充,如果这部内存一直没有修改,就是Clean RAM。一般apk、so、tff等程序或资源文件的内存映射区域就是Clean RAM。Clean RAM可在必要时进行回收,没有影响,不会造成数据丢失。

Dirty RAM
相关,如果内存的内容有修改过,就是Dirty RAM。 参照linux的说法:Dirty means "might need writing to disk or swap." 就是说这部分内存要么一直占着,如果要回收使用,必须将其内存写到磁盘或交换区,否则就会造成数据丢失。

Private RAM和Shared RAM、Clean RAM和Dirty RAM都是针对堆(heap)内存而言的。结合这两个的两个角度,可以有4种类型堆内存。

私有内存(Private)一般是Dirty的,如虚拟机或native中通过new分配的内存,也有Clean的,如so或dex的内存映射区域。共享内存(Shared)一般都是Clean的,如共享的代码或资源,也有Dirty,比较少。

如果共享的内存是Dirty(有修改)的,这个修改会影响到共享这个内存的所有进程(类似于进程间的全局状态量)。有一种情况是:Clean的共享内存,如果其中一个进程要修改,这个进程会复制一份出来,成为私有Dirty内存,这样修改就不会影响其他共享进程。

二、app占用内存分析

查看app运行时的内存占用情况,android开发工具提供了一个命令,这个命令的输出基本描述app所占用内存的信息,不同版本系统的输出略有差异。

adb shell dumpsys meminfo <package_name|pid> [-d]
//The -d flag prints more info related to Dalvik and ART memory usage.

运行测试的app,在命令行输入下面的命令,就可以看到app的占用内存的基本信息。

//Nexus 5 Android 4.4.4
$ adb shell dumpsys meminfo com.sample
Applications Memory Usage (kB):
Uptime: 9384089 Realtime: 16290323

** MEMINFO in pid 4893 [com.sample] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap     2181     2164        0        0     5616     4895      228
  Dalvik Heap     1664      960        0        0    17228    17130       98
 Dalvik Other     1175     1076        0        0                           
        Stack       76       76        0        0                           
    Other dev     1086     1040        4        0                           
     .so mmap      731      364        0        0                           
    .apk mmap       57        0        0        0                           
    .ttf mmap        2        0        0        0                           
    .dex mmap     1007       68      732        0                           
   Other mmap        5        4        0        0                           
     Graphics    41056    41056        0        0                           
           GL     3344     3344        0        0                           
      Unknown       72       72        0        0                           
        TOTAL    52456    50224      736        0    22844    22025      326
 
 Objects
               Views:       47         ViewRootImpl:        1
         AppContexts:        3           Activities:        1
              Assets:        2        AssetManagers:        2
       Local Binders:        7        Proxy Binders:       15
    Death Recipients:        0
     OpenSSL Sockets:        0
 
 SQL
         MEMORY_USED:        0
  PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0

Applications Memory Usage (kB):所有内存大小数值单位是kB。

Uptime: 9384089 Realtime: 16290323:upTime是系统启动以来的时间,不包括休眠时间;Realtime是也是启动时间,但包括休眠时间。upTime与Realtime单位都是微秒。

Native Heap
Native是在c/c++层分配的地址空间。如用malloc或mmap分配的内存空间,就会计算到Native Heap。

Dalvik Heap
在虚拟机(Dalvik)中分配的内存,java中new对象就是从Dalvik Heap分配内存。Dalvik Heap是内存的再次分配,Dalvik先申请一块Native的堆空间(好像是通过mmap分配的),然后将这块空间再次分配给new的对象。GC就是将不再被引用的对象回收,释放对象占用的内存空间。

其他的都是系统创建与使用的一些内存空间,可能了解下。 Dalvik Other:虚拟机其他内存空间。
Stack:栈空间。
Other dev:安卓属性系统服务和安卓进程间通信binder机制所用的设备文件内存映射。
.so mmap:so(native动态链接库)的内存映射空间。
.apk mmap:apk包文件的内存映射空间。
.ttf mmap:字体文件的的内存映射空间。
.dex mmap:dex文件的内存映射空间。
Other mmap:包括全部其他非匿名内存映射的东东。例如:安卓框架动态链接库/system/framework/.odex; unicode编码动态链接库/system/usr/.dat等; [vectors]貌似ARM特有的东西;代码段/system/bin/app_process;程序中通过执行系统调用运行的链接器/system/bin/linker。
Graphics:UI渲染占据的内存空间。每个Activity都有一个Surface,Surface会引用绘制缓存。
GL:OpenGL??
Unknown: 其他??

有一个测试的妹子(有时发现测试对有些问题的研究比开发还有深度)对app用到内存总结了一张表,挺全面的,可以参考下。
Android MemInfo

三、Dalvik Heap

对于android开发,重点要关注Dalvik Heap,在java层运行的代码的内存分配与释放都在Dalvik Heap。内存相关的问题大都由Dalvik Heap引发的。

前面说过,Dalvik Heap是内存的再次分配,相当于一个内存池。app进程都是从进程由zygote中fork出来的,进程应该有虚拟机初始化的逻辑。虚拟机可看成进程中一个native模块(或组件),它会向系统申请一块内存,这块内存用于java对象的空间分配。所有分配的java对象虚拟机都有记录,虚拟机中有一个专门的GC线程,它会根据规则检查java对象的引用关系,如果有对象没有再被引用,它就会主动回收这个java对象,释放它占有的内存空间。GC机制在一定程度上解决了C++中面临时的内存问题,简单化的程序开发、调试的难度,但也有一定的负作用。在后面的文章中专门讲下GC机制与问题。

Dalvik Heap是虚拟机中的内存空间,它的大小也是有一定限制,因为内存资源总是有限的。这也是OOM问题的来由。Dalvik Heap的大小因手机和系统而异,可以通过系统接口获取(单位是MB)。

int maxMemory = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
int largeMaxMemory = ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getLargeMemoryClass();
Log.d(TAG, "maxMemory:" + maxMemory);
Log.d(TAG, "largeMaxMemory:" + largeMaxMemory);   

除了普通堆大小,还有一个Large Heap大小,对于内存密集的APP可以使用Large Heap。Large Heap可在工程的manifest文件中配置。在Nexus 4(4.4.4)手机上,普通堆大小与Large Heap大小,分别为:

09-07 20:12:14.498 13901-13901/? D/MainActivity: maxMemory:192
09-07 20:12:14.498 13901-13901/? D/MainActivity: largeMaxMemory:512

不能把Large Heap作为解决内存问题的第一手段,设置Large Heap虽然可以解决一些问题,但会导致APP的后台的存活率降低。作为合格的程序员,我们的任务是用有限的内存实现无限的功能。问题总是会有,但方法总比问题多,下面会简单说下常见的内存问题,每个内存问题会在后面结果实际的例子进行详细的分析。

三、常见内存问题

内存泄露(Memory Leak)

内存溢出(Out of Memory)

内存占用过多

GC导致性能下降

【参考资料】
http://www.programering.com/a/MDNyEzNwATg.html
http://stackoverflow.com/questions/2298208/how-do-i-discover-memory-usage-of-my-application-in-android
https://developer.android.com/studio/profile/investigate-ram.html#LogMessages https://developer.android.com/training/articles/memory.html#Android
内存动态分配函数malloc的基本实现原理

Clone this wiki locally