在任何软件开发环境中,RAM都是非常宝贵资源。在移动操作系统里,由于物理内存的限制,它会变得更加的宝贵。虽然Android的Dalvik虚拟机会常规的执行垃圾回收,但是开发人员仍然不能忽略什么时候、在哪里申请和释放内存资源。
为了能够使垃圾回收器从应用里正常的回收内存资源,开发人员需要避免产生内存泄露,注意在合适的时候释放引用Reference(内存泄露常常由于保持着全局变量的引用)。对于大多数应用,Dalvik垃圾收集器会处理大部分的回收工作:系统会在对应脱离活动线程的作用域后回收你申请的内存资源。
一、 如何进行内存监测分析
1. 通过垃圾搜集日志进行分析
分析应用内存最简单的方式是检测Dalvik的日志,每一次垃圾收集,logcat会打印下面格式的日志:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
如:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
l GC Reason:是什么触发了垃圾收集、是那种类型的垃圾收集。如GC_CONCURRENT代表内存快填满时申请内存而触发的一次并发垃圾收集。GC_FOR_MALLOC代表内存占用已满时申请内存而触发的一次中断性垃圾收集,系统需要停止你的应用的执行来进行内存回收。GC_HPROF_DUMP_HEAP代表你分析内存时创建了一个内存HPROF文件导致垃圾收集。GC_EXPLICIT:一次明确执行的垃圾收集,如你调用了gc()方法(请尽量不要这么作)GC_EXTERNAL_ALLOC:只发生在API level 10或者更低的版本上,应用外分配内存的垃圾收集,如本地内存里存储的像素数据、NIC的字节缓存。
l Amount feed:垃圾收集回收的内存大小
l Heap stats:可用内存比例
l External memory stats:外部申请的内存量/内存回收触发阈值,仅在API level 10或者低版本有
l Pause time:中断时间。并发垃圾收集时会中断两次,分别在垃圾收集的开始和快结束时。
重点关注65% free 3571k/9991k的比例变化,如果持续增长而未能回收代表存在内存泄露。
2. 可视化观察内存变化
为了得到你的应用正在使用什么类型的内存什么时候使用的信息,可以通过设备监控程序来实时的浏览你应用的内存变化,操作步骤:打开设备监控(<sdk>/toos/monitor) =》 左边选择应用进程 =》 左上角点击”Update Heap” =》 右侧点击”Heap”。
Heap视图可以展示你应用内存使用的统计信息,并在每次垃圾搜集后进行更新。通过观察可以得到内存分配回收的实时信息从而帮助你决策合适申请和释放内存资源。
3. 跟踪应用内存分配
当你缩小了内存问题的范围时,你可以通过Allocation Tracker来更好的理解内存的分配及分布,它不仅可以观察特殊内存的使用,还可以用来分析应用分配内存的关键代码路径。
比如,在你的应用里拖动List时跟踪内存分配情况可以使你观察的这个动作发生时的全部内存分配情况、什么样的线程在运行、内存分配是从哪里触发的。这对于你需要通过减少代码路径来减少工作提升界面平滑性的场景十分有价值。程序启动路径如下:打开设备监控(<sdk>/toos/monitor) =》 左边选择应用进程 =》右侧点击”Allocation Tracker” =》 点击”Start Tracking” =》 想要更新时点击”Get Allocations”。
列表中显示了最近分配的全部内存,当前限制在512个实例缓存。点击分配的内存对象信息可以看到分配内存的代码执行堆栈路径。这不仅跟踪展示了分配了什么类型的对象内存,还展示了线程信息、类信息、哪个文件哪行代码。
4. 观察应用整体内存分配情况
为了更深入的分析,你可能需要观测你的应用使用的不同类型的内存的分布情况,此时可以通过命令:adb shell dumpsys meminfo <package name>,输出会已K为单位展示应用当前分配的全部内存,通常包括如下两类:
l 私有内存Private(Clean和Dirty的):你的应用进程单独使用的内存,代表着系统杀死你的进程后可以实际回收的内存总量。通常需要特别关注其中更为昂贵的dirty部分,它不仅只被你的进程使用而且会持续占用内存而不能被从内存中置换出存储。你申请的全部Dalvik和本地heap内存都是dirty的,和Zygote共享的Dalvik和本地heap内存也都是dirty的。
l Proportional Set大小(PSS):这是加入与其他进程共享的分页内存后你的应用占用的内存量,你的进程单独使用的全部内存也会加入这个值里,多进程共享的内存按照共享比例添加到PSS值中。如一个内存分页被两个进程共享,每个进程的PSS值会包括此内存分页大小的一半在内。
PSS的特性使得可以通过累加全部进程的PSS值来衡量多个进程真正使用的内存量。这是与其他进程比较内存实际使用量的真实指标。命令输入的字段的解释如下:
l Dalvik Heap:Dalvik虚拟机使用的内存。Pss值包括了Zygote申请的内存在内(会按共享的进程数按比例计算),Private Dirty是你的应用真实单独使用的内存,包括从Zygote进程fork出你的进程后你的应用和Zygote曾经修改过的内存。
l Davik Other:如JIT和垃圾收集之类的Davik占用内存。如果没有此选项行证明是老的版本,上面的内存占用会被累加到Dalvik Heap里。
l Heap Alloc:累加了Dalvik和Native的heap,因此会比Dalvik Heap大。因为你的进程是从Zygote中fork出来的,所以包括了与其他进程共享的内存。
l .so mmap及.dex mmap:加载本地so代码合Dalvik虚拟机dex代码的映射占用的内存。同理PSS Total包括了与其他进程共享的代码的内存占用。Private clean是自己应用代码占用的内存。但是通常情况下,真实的内存映射尺寸要更大一些,这个值只代表当前需要加载到内存里的执行过的代码占内存大小,然而.so被加载到最终的地址时需要固定连接到本地代码会占用大量private dirty。
l Unknown:无法归类到其他类别里内存占用,通常是一些无法被工具识别的本地内存分配,类似Dalvik Heap,也包括 Pss Total和Private dirty。
l TOTAL:上面全部条目的累加值,全局的展示了你的进程占用的内存情况,可以用来与其他进程进行比较。Clean内存是从持久化的文件(如执行过的代码)映射占用的内存,如果一段时间比使用会被置换掉。
l ViewRootImpl:你的应用进程里的活动窗口视图个数,可以用来监测对话框或者其他窗口的内存泄露。
l AppContexts及Activities:应用进程里Context和Activity的对象个数,可以用来监测Activity的内存泄露,通常是由于一个静态的引用导致,如View和Drawable都可以持有起始的Activity的引用。
5. 进行内存dump分析
内存dump包含了应用内存里的全部的对象信息,通常存储为二进制的HPROF文件,可以通过分析这个文件找寻内存问题。DDMS里点击”Dump HPROF file”即可dump内存,然后点击Save保存。也可以通过在代码里调用dumpHprofData()来更精确的控制dump内存时机。与Java的dump稍有不同,Android的内存dump含有大量的Zygote分配的内存对象,由于是多进程共享的,通常不需要特殊分析。
可以使用MAT来分析dump文件,但要首先转换为J2SE标准的格式。可以使用<sdk>/platform-tools/内的hprof-conv工具实现,简单使用原始文件和转换后的文件两个参数执行即可:hpfor-conv heap-original.hprof heap-converted.hprof。注意如果使用的是Eclipse插件版本的DDMS时系统会自动帮你转换。
通常,分析内存时注意检查如下点:
l 对Activity/Context/View/Drawable的长期存在的引用或者间接引用
l 非静态的内部类,如Runnable,会持有Activity实例的引用
l 长期持有对象引用的缓存
MAT是能力非常强大的内存分析工具,打开内存dump时通常会首先看到内存分布的饼图,可以方便的看到占用内存最大的对象。除此之外,常用的两个菜单功能有:Histogram view,罗列全部的类和他们的实例数,可以通过邮件选择List object > with incomming查看导致无法释放的引用的来源,通过Path To GC Roots>exclude weak references查看到根引用的另。Dominator tree可以按照对象占用的内存大小罗列,同样可以通过查询到根引用的路径分析引用情况。具体使用可以参数google的视频http://www.youtube.com/watch?v=_CruQY55HOk和MAT的文档http://wiki.eclipse.org/index.php/MemoryAnalyzer。
MAT另一个强大的功能是进行多个dump的对比,可以通过把每个dump打开后执行Window>Navigation History>右键Histogram>Add to Compare Baset,最后执行Compare the Result来对比。
6. 进行触发内存泄露的测试
使用上述工具前需要压测你的应用直到内存泄露产生,通常需要运行一段时间后进行分析,大的内存泄露通常会在分配的最大内存部分看到,越小的内存泄露需要越长时间进行测试。你可以手动或者使用monkey尝试如下两种方法重现内存泄露:
l 在不同的Activity内持续旋转设备,因为旋转设备会触发Activity重绘。
l 在不同的Activity状态里在不同的应用之间跳转。
二、 备注及资料
1. Paging
分页是操作系统管理内存的一种方案,通过从第二存储区存取数据来使用主存。通过第二存储区读写的数据会被分隔为相同大小的块(起名叫分页)。分页的最大好处是使得进程使用的物理内存地址不再连续,否则系统要将整个应用程序或者程序的整个部分连续存储到内存,而这非常容易产生多样的存储和碎片问题。
Windows NT类系统使用pagefile.sys文件来分页,默认存储在Windows安装的根目录。
Unix或者Unix类系统使用swap来实现类似功能,swap既、用来在内存和硬盘之间移动数据又用来存储在硬盘上的分页信息。通常会使用整个磁盘分区来作为swap,这些分区叫做swap分区。
Linux内使用swap文件同样可以达到Unix的swap分区的速度,但是swap文件的限制是需要在文件系统内来申请空间。为了提高性能,Linux内核保存了一份swap文件在设备上的位置目录信息的Map缓存,从而避免了文件系统的超负荷过载。Red Hat认为还是应该使用swap分区,因为可以将分区存储到磁盘的高速读取和搜索的位置,从而充分利用磁盘提高性能。但是swap文件的灵活性远远大于swap一个磁盘分区,可以被存储在文件系统任何位置,可以增加、修改。
2. Swap
Linux的Swap可以在物理内存不足时派上用场,此时可以将物理内存的一部分空间释放出来供当前运行的程序使用。被释放的空间通常是很长时间没有什么操作的程序,这些被释放的空间会保存到Swap空间中,等到需要运行时再从Swap中恢复。
3. Memory-mapped file
内存映射文件是一块虚拟的内存区域,被分配用来提供与文件(或类似文件类的资源)进行字节到字节的关联。典型的映射资源是存储在磁盘上的物理文件,但也有可能是一个设备、共享的内存对象、或者其他通过file descriptor可以描述的系统资源。一旦有了这种文件和内存的映射关联,应用程序可以像访问内存一样访问这块映射区域。
内存映射文件的最大好处是提供了I/O性能,尤其是使用大文件时。不过小的文件会造成空间浪费,因为内存映射大小会被设置为分布到多个分页上(每个分页大都是4K),这样5K的文件需要两个分页占用了8K空间。内存映射文件同时提供了懒加载的能力,使得一个巨大的文件可以使用很少的内存。
内存映射文件使用最广泛的地方是操作系统(windows和unix),系统进程启动后,系统会使用内存映射文件来将可执行文件和其模块读入内存来执行。内存映射系统通常会使用demand paging技术,只会加载真正需要执行的文件。
内存映射文件另一个广发应用是在多进程中共享内存。在当今受保护模式类的操作系统里,一个进程是不允许访问另一个进程使用的内存区域的,内存映射文件I/O是最常用的解决这个问题的方式,使得多个进程可以映射同一个物理文件然后通过内存访问。
4. 资料来源
android-sdk/docs/tools/debugging/debugging-memory.html
https://en.wikipedia.org/wiki/Paging
https://en.wikipedia.org/wiki/Memory-mapped_file