Android内存知识及用MAT工具定位内存泄漏的方法

Java内存

 Java管理的内存分两种, 堆和栈. 栈是保存函数形参和局部变量的地方, 栈里保存的是对象的引用. 一个正在执行的函数总是存放在栈的最
 上层.每个线程都有自己的调用栈.  堆上存放着所有java程序通过”new”操作符创建的对象, 数组也存放在堆里.  Java虚拟机把堆按照
 某种逻辑分成不同的区域来存放对象, 以便垃圾回收器能更快速有效的回收这些对象.


Native内存

 Native内存是进程可以获取的内存, Native内存由操作系统控制,是由物理内存和其它物理设备比如硬盘,闪存等提供的. 通过JNI申请的内
 存属于Native内存. Native堆内存并不会引起OutOfMemory错误.

Heli030.png

应用内存查看命令

通用命令

1、adb shell dumpsys meminfo -- 查看当前系统所有进程的内存占用情况。

2、adb shell dumpsys meminfo PID -- 查看指定进程的内存占用的情况

3、adb shell cat /proc/PID/smaps -- 查看指定进程的内存占用的详细情况

4、adb shell showmap PID -- 查看指定进程的内存占用的详细情况。

进阶命令 -- 需要从eng版本的手机上将system/lib/libpagemap.so放进手机,
            并将xbin的procrank、librank、procmem放进手机,并chmod 777该文件才可以试用下面的命令。

1、adb shell procrank -- 查看当前系统所有进程的内存占用情况。

2、adb shell librank -- 查看当前系统内存的分布情况。

3、adb shell procmem PID -- 查看指定进程的内存占用的详细情况。

应用内存泄漏相关文件的导出命令

手机预置条件 -- root手机后,设置系统属性,adb shell setprop sys.vivo.dumphprof 1 

1、hprof文件导出 -- 虚拟机内存泄漏的内存分析文件
   adb shell am dumpheap PID /data/local/tmp/PACKGE.hprof    -- 导出指定进程的hprof文件
   adb shell am dumpheap -n PID /data/local/tmp/PACKGE.hprof -- 导出制定进程的native-dalvik的hprof文件

   以上hprof文件导出后,需要进行转换,通过下载的SDK的“SDK\platform-tools\hprof-conv.exe”文件进行转换
   转换命令为: SDK\platform-tools\hprof-conv.exe PACKGE.hprof out.hprof
   装转换出来的out.hprof文件通过eclipse的mat插件打开即可得到相关的内存信息。
  

2、smaps文件导出 -- native内存泄漏的内存分析文件
  adb push /proc/PID/smaps -- 导出指定进程的smaps文件,用以分析非虚拟机内存泄漏的内存文件信息。
  这里面的是进程内存的详细分配情况的说明。可以找到内存泄漏的怀疑点从而进一步处理。

Android内存的一些基础知识

1、进程的地址空间
  Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小
  Heap空间的使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。
  正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。

2、进程内存空间和RAM之间的关系
  进程的内存空间只是虚拟内存(或者叫作逻辑内存),而程序的运行需要的是实实在在的内存,即物理内存(RAM)。
  在必要时,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。
  RAM作为进程运行不可或缺的资源,对系统性能和稳定性有着决定性影响。
  另外,RAM的一部分被操作系统留作他用,比如显存等等,内存映射和显存等都是由操作系统控制,我们也不必过多地关注它,进程所操作的空间都是虚拟地址空间,无法直接操作RAM。

3、Android中的进程
  (1)native进程:采用C/C++实现,不包含dalvik实例的进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的
  (2)java进程:Android中运行于dalvik虚拟机之上的进程。
       dalvik虚拟机的宿主进程由fork()系统调用创建,所以每一个java进程都是存在于一个native进程中,因此,java进程的内存分配比native进程复杂,
       因为进程中存在一个虚拟机实例。

4、Android中进程的堆内存
  heap空间完全由程序员控制,我们使用的malloc、C++ new和java new所申请的空间都是heap空间, C/C++申请的内存空间在native heap中,而java申请的内存空间则在dalvik heap中。

5、Android的 java程序为什么容易出现OOM
  这个是因为Android系统对dalvik的vm heapsize作了硬性限制,当java进程申请的java空间超过阈值时,就会抛出OOM异常,
  可以通过adb shell getprop | grep vm 查看此值。
  这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。
  迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。

6、Android如何应对RAM不足
  java程序发生OMM并不是表示RAM不足,如果RAM真的不足时Android系统中的的lowmemorykiller会起作用,当RAM所剩不多时,
  lowmemorykiller会杀死一些优先级比较低的进程来释放物理内存,让高优先级程序得到更多的内存。
  而对于应用来说,继承onLowMemory()方法也可以在RAM不足时调用,可以在该方法做一些保存数据或者是清应用内存的操作,
  同时对于不同level的onTrimMemory()方法的继承,处理不同RAM状态下的内存回收,对于应用的内存释放还是能起到比较理想的作用。

7、如何查看RAM使用情况
  可以使用adb shell cat /proc/meminfo查看RAM使用情况
  这里对其中的一些字段进行解释:
     MemTotal:可以使用的RAM总和(小于实际RAM,操作系统预留了一部分)
     MemFree:未使用的RAM
     Cached:缓存(这个也是app可以申请到的内存)
     HightTotal:RAM中地址高于860M的物理内存总和,只能被用户空间的程序使用。
     HightFree:RAM中地址高于860M的未使用内存
     LowTotal:RAM中内核和用户空间程序都可以使用的内存总和(对于512M的RAM: lowTotal= MemTotal)
     LowFree: RAM中内核和用户空间程序未使用的内存(对于512M的RAM: lowFree = MemFree)

8、如何查看进程的内存信息
  1)、使用adb shell dumpsys meminfo + packagename/pid
  2)、使用adb shell procrank查看进程内存信息
  解释一些字段的意思:
     VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
     RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)
     PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
     USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
     一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

9、应用程序如何绕过dalvikvm heapsize的限制
 对于一些大型的应用程序(比如游戏),内存使用会比较多,很容易超超出vm heapsize的限制,这时怎么保证程序不会因为OOM而崩溃呢?
 1)、创建一个新的进程,那么我们就可以把一些对象分配到新进程的heap上了,从而达到一个应用程序使用更多的内存的目的。
      当然,创建子进程会增加系统开销,而且并不是所有应用程序都适合这样做,视需求而定。
      创建子进程的方法:使用android:process标签。
 (2)、使用jni在native heap上申请空间

10、Bitmap分配在native heap还是dalvik heap上?
 bitmap对象是通过env->NewOject( )创建的,到这里疑惑就解开了,bitmap对象是虚拟机创建的,JNIEnv的NewOject方法返回的是java对象,并不是native对象,所以它会分配到dalvik heap中。
 
11、java程序如何才能创建native对象
 必须使用jni,而且应该用C语言的malloc或者C++的new关键字。
 new或者malloc申请的内存是虚拟内存,申请之后不会立即映射到物理内存,即不会占用RAM,只有调用memset使用内存后,虚拟内存才会真正映射到RAM。

内存分配

1、内存分配类型
从adb shell dumpsys meminfo PID, 可以找到下面的这些内存类型。
再结合adb shell cat proc/PID/smaps的内存可以找到具体的文件信息。

Naitve Heap Size: 从mallinfo usmblks获得,代表最大总共分配空间

Native Heap Alloc: 从mallinfo uorblks获得,总共分配空间

Native Heap Free: 从mallinfo fordblks获得,代表总共剩余空间 

Native Heap Size 约等于Native Heap Alloc + Native Heap Free

mallinfo是一个C库, mallinfo 函数提供了各种各样的通过C的malloc()函数分配的内存的统计信息。

Dalvik Heap Size:从Runtime totalMemory()获得,Dalvik Heap总共的内存大小。

Dalvik Heap Alloc: Runtime totalMemory()-freeMemory() ,Dalvik Heap分配的内存大小。

Dalvik Heap Free:从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小。

Dalvik Heap Size 约等于Dalvik_Heap_Alloc + Dalvik_Heap_Free

OtherPss, include Cursor,Ashmem, Other Dev, .so mmap, .jar mmap, .apk mmap, .ttf mmap, .dex mmap, Other mmap, Unkown统计信息都可以在process的smaps文件看到。

Objects and SQL 信息都是从Android Debug信息中获得。

其他类型               smap 路径名称          描述

Cursor               /dev/ashmem/Cursor     Cursor消耗的内存(KB)

Ashmem               /dev/ashmem            匿名共享内存用来提供共享内存通过分配一个多个进程
                                            可以共享的带名称的内存块

Other dev             /dev/                 内部driver占用的在 “Other dev”                                                 

.so mmap             .so                    C 库代码占用的内存

.jar mmap            .jar                   Java 文件代码占用的内存

.apk mmap           .apk                    apk代码占用的内存

.ttf mmap              .ttf                 ttf 文件代码占用的内存

.dex mmap             .dex                  Dex 文件代码占用的内存

Other mmap                                  其他文件占用的内存

2、内存分配分析
对于应用自身的内存问题,可以先通过一下方法判断内存时候有问题:

1)在测试前,先保存一份adb shell cat proc/PID/smaps 和一份hprof文件

2)根据测试用例,跑一段时间后,再抓取一份smaps文件和hprof文件

3)根据测试用例,跑完最后的步骤,最后抓取一份smaps和hprof文件

4)根据抓取的三份不同文件,对比分析不同阶段的smaps文件、hprof文件。找出可能出现内存增长不合理的地方,从而做进一步的处理。

内存泄漏

 一个对象一旦没有任何对象引用指向它, 那么java虚拟机的垃圾回收器就会将其回收, 释放其所占的内存. 如果还有其它对象持有指向它的
 引用, 则垃圾回收器就不能将其回收. 当对象不再继续使用, 而垃圾回收器又无法将其回收时, 就发生了内存泄漏.



MAT工具介绍

 a. 通过eclipse获取hprof文件. 在ddms里选择要分析的进程, 点击dump hprof按钮, 则eclipse会自动生成hprof文件并打开.

Heli031.png


 b. 在代码中使用android.os.Debug.dumpHprofData("/data/temp/dump.hprof");获得hprof文件, 获得的hprof文件需要使用
 android-sdk\tools\下的hprof-conv.exe工具转换后才能被MAT打开. 

Heli032.png


 打开hprof文件可以看到一个概览图.

Heli033.png


 Leak Suspects, Histogram和Dominator Tree是最常用的3个分析图表. 
 Leak Suspects是MAT做的自动分析, 提示了它怀疑泄漏的地方.

Heli034.png


 Histogram很容易看出那些数量多或者体积大的对象, 它列出了所有的类和数组.

Heli035.png


 Class Name:类名
 Objects:每一种类型的对象数量
 Shallow Heap:一个对象本身(不包括该对象引用的其他对象)所占用的内存
 Retained Heap:一个对象本身,以及由该对象引用的其他对象的Shallow Heap的总和


 列表的第一行是一个搜索框, 可以输入表达式来过滤列表的内容.

Heli036.png


 Hprof记录的是某个时间点的heap内存情况, 因此, Histogram提供了一个对比两份Hprof文件的功能, 即对比两个不同时刻.

Heli037.png

 Dominator Tree 列出了进程中的所有对象.

Heli038.png

 GC Roots:简单的理解就是指那些不会被垃圾回收的对象. 垃圾回收器就是从它开始往下搜索引用链的, 没有链接到GC Roots的对象即是
 可被垃圾回收器回收的。 图中标识有黄色圆点的对象就是GC Roots,每个GC Root之后都会有灰黑色的标识表明这个对象之所以是GC Root
 的原因。
 选中一个对象, 点击右键, 选择” Path To GC Roots”菜单, 即可查看该对象到GC Roots的引用链.

Heli039.png

Heli040.png


 MAT提供了一种叫做OQL(Object Query Language,对象查询语言)的工具,方便用于按照自己的规则过滤对象数据。
 例如想查询我的Activity的所有对象:
 Select * from com.example.com.bbk.meminfo.MainActivity.

Heli041.png


MAT分析思路

 分析的基本过程就是:找到那些数量很大或者体积很大的对象,通过“Path To GC Roots”的引用链找到他们被什么样的GC Roots引用而
 没有被释放,然后再通过检查代码逻辑找到问题原因, 去掉那些该去掉的引用.
 思路一:
 根据Leak Suspects的提示, 直接在Dominator Tree中的第一列过滤com.example.com.bbk.meminfo.MainActivity(或者使用OQL查
 询)找到该对象, 然后展开该对象, 可看到byte[10][], 就是提示里的byte[][]. 

Heli042.png

Heli043.png


 查看“Path To GC Roots”引用链, 可看到只在MainActivity有引用, 最后到MainActivity里检查代码逻辑.

Heli044.png


 思路二:
 Leak Suspects没有提示, 则可到Histogram查看对象数量最多的那些类型, 再到Dominator Tree查找该类型对象, 然后再通过“Path
  To GC Roots”分析. 或者在Dominator Tree里按Retained Heap降序排列, 查看Retained Heap最大的那些对象, 然后再往下分析. 
 或者使用OQL列出该进程package的所有对象,然后再查找可疑的对象, 往下分析.

Heli045.png

猜你喜欢

转载自blog.csdn.net/zhiwenwei/article/details/68921680