Android应用内存泄露分析

jopen 9年前

   原文地址http://developer.android.com/tools/debugging/debugging-memory.html

       因为Android是为移动设备设计的,所以我们应该一直注意应用使用了多少内存。尽管Dalvik虚拟机会进行常规的垃圾回收,这并不意味这可以忽略 应用内存的分配和释放。为了提供一个稳定的用户体验,使app之间迅速的进行切换,当用户不与应用交互时应该减少不必要的内存消耗。
       尽管在开发时遵守了管理内存的最佳方法,但我们仍可能内存泄露或者引起其他内存的问题。确定应用使用了尽可能少内存的方法是使用工具分析应用的内存,下面展示了几种分析方法。

  • 解读Log信息
        Dalvik的log信息是最简单的分析应用内存的地方,每一次垃圾回收,logcat会打印以下格式的信息:
        D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,<External_memory_stats>, <Pause_time>

        GC原因

   垃圾手机被触发的原因和类型有以下几种:

  GC_CONCURRENT

   当堆内存将要满的时候进行的并发的垃圾回收以释放内存

  GC_FOR_MALLOC

   当堆内存已经满时,app尝试分配内存会引发的垃圾回收,这时候系统会停止应用执行来回收内存

  GC_HPROF_DUMP_HEAP

   当创建一个HRPOF文件来分析内存时引起的垃圾回收

  GC_EXPLICIT

   明确的垃圾回收,也就是调用gc()(应该避免调用这个方法,相信垃圾回收器在需要的时候会执行)方法会执行的垃圾回收

  GC_EXTERNAL_ALLOC

   这个只存在于API 10或者小于10(新版本任何对象都在Dalvik堆中进行分配),对外部分配内存的垃圾回收(比如存在于native内存的像素数据)


     释放量

    此次垃圾回收释放的内存量

     堆信息统计

    未被占用内存的百分比和(存活对象占内存的大小/堆总共的大小)

    外部内存统计

    在API10及其以下分配的外部内存,(分配的内存大小/限制值,当超过这个限制时会进行回收)

    暂停时间

    更大的堆会有更长时间的暂停。并发的暂停时间显示两个暂停:一个是在回收开始的时候,另一个是在接近结束时。

    D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external4703K/5261K, paused 2ms+2ms
    不断观察log中的这些信息,注意堆状态信息(上面的3571K/9991K)的增长,如果这个值不断在增长,没有变小,说明存在内存泄露。

  • 查看堆更新
    为了获取应用使用内存的种类类和分配时间,你可以通过Device Monitor来查看应用堆的即时更新:

    1.打开DeviceMonitor sdk/tools/monitor

    2.在 DebugMonitor window,选择应用的进程

    3.点击进程列表上面的UpdateHeap按钮

    4.在面板的右边选择Heap页

 Heap视图显示了堆内存的基本使用情况,会在每一次的垃圾回收后进行更新。点击Cause GC按钮来看第一次的更新


   图 DeviceMonitor工具,1为Update Heap,2为Cause GC按钮

       不断与应用进行交互,在垃圾回收后来查看堆内存分配情况,这可以帮助你找出哪些操作可能引起太多的内存分配,在哪些地方来尝试进行减少分配和释放资源。

  • 跟踪分配

       当你开始缩小内存问题的范围后,你应该使用Allocation Tracker来确认问题对象是在哪里分配的。AllocationTracker不仅对寻找内存的具体使用有帮助,而且对分析关键代码的执行路径很有帮助

       比如跟踪应用list滑动时的内存分配,你可以查看那个操作所有的内存分配,在哪个线程,在哪里分配。这是非常有用的,通过对调用路径的分析,来减少不必要的工作,提高UI的流畅度。

         使用方法:

    1.打开DeviceMonitor

    2.在DDMS窗口,选择要观察的进程

    3.在右边的面板上,选择AllocationTracker页

    4.点击StartTracking.

    5.与app进行交互,以便你想要分析的代码执行

    5.当你想要更新分配列表时,点击Get Allocations按钮


       这个列表显示了所有最近的分配对象,目前被512条数据的环形缓冲区限制。选择一行来查看导致对象分配的方法调用栈,栈不仅显示了分配对象的类型,还显示了在哪个线程,哪个类,哪个文件的哪一行,非常强大。

   尽管不可能移除所有UI关键代码路径的对象分配,allocation tracker 可以帮助你分析你代码的问题所在。举个例子,一些应用可能在draw方法每一次执行的时候都创建一个新的paint,把这个对象改为global可以帮助提升性能。


  • 查看内存的整体分配

    进一步的分析,你可能想知道应用的内存都被哪些不同类型的内存分配占用了,通过下面的adb命令即可查看:

   adb shell dumpsys meminfo<package_name>

</blockquote>

   这个命令会列出应用目前的内存分配情况,单位是KB

   当分析这些信息时,你应该熟悉下面几种类型的分配:

   Private (Clean and Dirty) RAM

       这些内存是只被你的进程使用的。这是当你的应用被销毁后系统可以回收的内存量。通常,最重要的列是“private dirty”,它的消耗是非常昂贵的,因为只能被你的进程使用,并且它的内容只能存在于内存并且不能被交换到外部存储中(因为Android没有使用 swap)。所有本进程的Dalvik和native堆的分配都是privatedirty的内存,与Zygote进程共享的Dalvik和native 分配是shared dirty的内存。

   Proportional Set Size (PSS)

       这是应用的内存使用大小,包含了进程间共享的页。任何只属于你进程的内存页直接全部算在PSS值中,与其它进程共享的内存页,按照成比例的量算进PSS值中,比如,两个进程共享一个页,那么每一个进程的PSS都只增加该页大小的一半。

       PSS测量一个好的特性是可以将所有进程的PSS加起来确定所有进程的内存使用量,这意味这PSS是衡量进程内存占用,进程间内存使用对比和所有有效内存的好方法。

   例如,下面是Gmail进程在平板设备上的内存使用情况,里面有很多信息,但关键点是下面列的几个:

** MEMINFO in pid 9953 [com.google.android.gm] **                   Pss     Pss  Shared Private  Shared Private    Heap    Heap    Heap                 Total   Clean   Dirty   Dirty   Clean   Clean    Size   Alloc    Free                ------  ------  ------  ------  ------  ------  ------  ------  ------    Native Heap      0       0       0       0       0       0    7800    7637(6)  126    Dalvik Heap   5110(3)    0    4136    4988(3)    0       0    9168    8958(6)  210   Dalvik Other   2850       0    2684    2772       0       0          Stack     36       0       8      36       0       0         Cursor    136       0       0     136       0       0         Ashmem     12       0      28       0       0       0      Other dev    380       0      24     376       0       4       .so mmap   5443(5) 1996    2584    2664(5) 5788    1996(5)      .apk mmap    235      32       0       0    1252      32      .ttf mmap     36      12       0       0      88      12      .dex mmap   3019(5) 2148       0       0    8936    2148(5)     Other mmap    107       0       8       8     324      68        Unknown   6994(4)    0     252    6992(4)    0       0          TOTAL  24358(1) 4188    9724   17972(2)16388    4260(2)16968   16595     336     Objects                 Views:    426         ViewRootImpl:        3(8)           AppContexts:      6(7)        Activities:        2(7)                Assets:      2        AssetManagers:        2         Local Binders:     64        Proxy Binders:       34      Death Recipients:      0       OpenSSL Sockets:      1     SQL           MEMORY_USED:   1739    PAGECACHE_OVERFLOW:   1164          MALLOC_SIZE:       62

       通常,你应该关心的只用PssTotal和Private Dirty两列。在某些情况下,PrivateClean和Heap Alloc列也提供了有趣的数据。下面是不同内存分配种类的信息:

   Dalvik Heap

       应用中Dalvik使用的内存。PssTotal包含所有Zygote进程中的分配(在多个进程中共享根据权重来算),PrivateDirty是你的应用单独使用的堆内存,由应用自己的内存分配和从Zygote进程复制后来后又进行修改过的内存页。

       在比较新的版本里有DalvikOther项,Pss Total和Private Dirty显示的Dalvik Heap数据,并不包含Dalvik的开销,比如JIT和GC的开销,在旧版本上面都把它们归并到了Dalvik里。

   Heap Alloc

          Heap Alloc是Dalvik和native堆分配器跟踪应用app消耗的内存,这个值比Pss Total和Private Dirty大,因为你的进程是从Zygote进程复制的,它包含了与其他进程共享分配的内存。

 

.so mmap 和.dex mmap

   被 用来映射.so和.dex代码的内存,Pss Total包含了应用之间共享的平台性代码,Private Clean是应用自身代码消耗的内存,通常,实际的映射内存会更加大,这里显示的内存只是需要被app执行的代码所占用的大小,但是,.so映射会占用很 大的private dirty内存,因为当代码被加载到最后的地址时需要转换为本地代码。

Unknown

        任何系统不能分类到其他种类的内存页,目前,它包含了大部分native分配,因为ASLR技术的存在,不能被工具识别。就像Dalvik一样,Pss Total包含了与Zygote共享的部分,Private Dirty是只属于你的app的未知内存。

Total

       你的进程使用的Pss内存总共的大小,这是所有PSS列数据之和,它表明了你的进程所占用内存的大小,可以与其他进程和有效内存进行对比。

       Private Dirty和Private Clean是你进程的总共分配内存,它是不与其它进程共享的。它们加起来(特别是Private Dirty)是当进程被销毁后系统可以回收的内存。Dirty内存是那些被修改的必须保留提交到内存的页,clean内存是那些映射持久化文件到内存所占 用的页(比如被执行的代码),如果一段时间不被使用可以被交换出去。

 

ViewRootImpl

       你的进程中活跃的rootview的数量,每一个root view都和一个window关联,所有这可以帮助你区分涉及到dialog或者其他widows的内存泄露

AppContext和Activities

       你的进程中存在的Context和Activity对象的数量,这对快速识别由于静态引用导致Activity对象的泄露是很有帮助的,这些对象引用了 其他许多的对象,所以如果泄露会导致其他大量的内存不能被释放。一个View或者Drawable会保持一个对Activity的引用,所有保持一个 View或者Drawable不被释放也会导致你的app泄露一个Activity。