Android内存调试工具总结

索引

基础概念

img img img img

如果看进程独占内存, 则使用USS. 但是一般考虑到共享内存的情况, 大部分都是依据PSS查看对应进程的内存占用.

系统内存信息分析

logcat 输出

dalvik日志打印

场景: 在应用随机出现慢时, 可以看看那段时间logcat的输出, 是否包含GC的一些信息. GC可能会导致应用慢, 而系统发起的GC则意味着内存不够, 可能出现从磁盘加载的情况.

在logcat中会打印dalvik的垃圾回收信息.

GC_Reason: GC_CONCURRENT 在您的堆开始占用内存时可以释放内存的并发垃圾回收。 GC_FOR_MALLOC 堆已满而系统不得不停止您的应用并回收内存时,您的应用尝试分配内存而引起的垃圾回收。GC_HPROF_DUMP_HEAP 当您请求创建 HPROF 文件来分析堆时出现的垃圾回收。 GC_EXPLICIT 显式垃圾回收,例如当您调用 gc() 时(您应避免调用,而应信任垃圾回收会根据需要运行)。GC_EXTERNAL_ALLOC 这仅适用于 API 级别 10 及更低级别(更新版本会在 Dalvik 堆中分配任何内存)。外部分配内存的垃圾回收(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。 Amount_freed: 从此次垃圾回收中回收的内存量。

Heap_stats:

堆的可用空间百分比与(活动对象数量)/(堆总大小)。

External_memory_stats:

API 级别 10 (Android3.0 以下)及更低级别的外部分配内存(已分配内存量)/(发生回收的限值). 而新API中, 统一由dalvik管理.

Pause_time

堆越大,暂停时间越长。并发暂停时间显示了两个暂停:一个出现在回收开始时,另一个出现在回收快要完成时。

art日志打印

与 Dalvik 不同,ART 不会为未明确请求的垃圾回收记录消息。只有在认为垃圾回收速度较慢时才会打印垃圾回收。更确切地说,仅在垃圾回收暂停时间超过 5ms 或垃圾回收持续时间超过 100ms 时。如果应用未处于可察觉的暂停进程状态,那么其垃圾回收不会被视为较慢。始终会记录显式垃圾回收。

GC_Reason:

Concurrent 不会暂停应用线程的并发垃圾回收。此垃圾回收在后台线程中运行,而且不会阻止分配。 Alloc 您的应用在堆已满时尝试分配内存引起的垃圾回收。在这种情况下,分配线程中发生了垃圾回收。 Explicit 由应用明确请求的垃圾回收,例如,通过调用 gc() 或 gc()。与 Dalvik 相同,在 ART 中,最佳做法是您应信任垃圾回收并避免请求显式垃圾回收(如果可能)。不建议使用显式垃圾回收,因为它们会阻止分配线程并不必要地浪费 CPU 周期。如果显式垃圾回收导致其他线程被抢占,那么它们也可能会导致卡顿(应用中出现间断、抖动或暂停)。 NativeAlloc 原生分配(如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。CollectorTransition 由堆转换引起的回收;此回收由运行时切换垃圾回收引起。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,回收器转换仅在以下情况下出现:在 RAM 较小的设备上,应用将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。 HomogeneousSpaceCompact 齐性空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到可察觉的暂停进程状态时发生。这样做的主要原因是减少 RAM 使用量并对堆进行碎片整理。 DisableMovingGc 这不是真正的垃圾回收原因,但请注意,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面具有限制。 HeapTrim 这不是垃圾回收原因,但请注意,堆修剪完成之前回收会一直受到阻止。

GC_Name:

ART 具有可以运行的多种不同的垃圾回收。

Concurrent mark sweep (CMS) 整个堆回收器,会释放和回收映像空间以外的所有其他空间。Concurrent partial mark sweep 几乎整个堆回收器,会回收除了映像空间和 zygote 空间以外的所有其他空间。 Concurrent sticky mark sweep 生成回收器,只能释放自上次垃圾回收以来分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,因为它更快速且暂停时间更短。 Marksweep + semispace 非并发、复制垃圾回收,用于堆转换以及齐性空间压缩(对堆进行碎片整理)。

Objects_freed:

此次垃圾回收从非大型对象空间回收的对象数量。

Size_freed:

此次垃圾回收从非大型对象空间回收的字节数量。

Large_objects_freed:

此次垃圾回收从大型对象空间回收的对象数量。

Large_object_size_freed:

此次垃圾回收从大型对象空间回收的字节数量。

Heap_stats

空闲百分比与(活动对象数量)/(堆总大小)。

Pause_time(s)

通常情况下,暂停时间与垃圾回收运行时修改的对象引用数量成正比。当前,ART CMS 垃圾回收仅在垃圾回收即将完成时暂停一次。移动的垃圾回收暂停时间较长,会在大部分垃圾回收期间持续出现。

android自带工具

dumpsys meminfo

https://developer.android.com/studio/profile/investigate-ram.html http://gityuan.com/2016/01/02/memory-analysis-command/

场景: 查看dalvik等区域内存情况.

procrank

场景: 查看各个进程的PSS/USS 并比较

cat /proc/meminfo

场景: 系统内存详细信息

vmstat

场景: 监控系统状态.

该工具可以查看系统内存信息, 进程队列, 系统切换, CPU时间占比等信息, 周期性动态输出.

总共15列参数, 个参数含义如下:

更多更详细的信息, 请直接查看 /proc/vmstathttp://blog.csdn.net/macky0668/article/details/6839498

librank

场景: 与procrank互补使用, 是procmem的扩展. 显示所有进程的信息, 需要工具统计分类. 也可以用来查看每个库或共享文件真实占用的内存大小.

数据来源与procmem一样, 但是procmem针对的是单个进程的内存占用情况. 而librank是显示某个库或者文件被哪些进程共享, 并显示占用大小. 如下图所示.

imgimgimg

dumpcache

该工具是android7.0之后才有的工具,用于查看文件系统中有每个文件被cache到内存的大小. 该文件可以用来判断缺页错误时加载的文件.

在Launcher界面时的输出如下:

img

稳定播放1080P视频时:

img

按下遥控器确定键显示UI时:

img

对比, 可以知道在播放过程中, 显示UI时主要加载的文件是:

img

bugreport 信息分析

Android系统想要成为一个功能完备,生态繁荣的操作系统,那就必须提供完整的应用开发环境。而在应用开发中,app程序的调试分析是日常生产中进程会进行的工作。Android为了方便开发人员分析整个系统平台和某个app在运行一段时间之内的所有信息,专门开发了bugreport工具。

该工具的缺点是: 1. 耗时过长, 只能抓取当前状态. 2. 抓取过程中可能导致系统异常, 因为其oom会被调高, 在内存不足时导致系统其他应用被kill. 3. 由于bugreport的权限会降低到shell, 而很多服务在会拒绝dumpsys的操作, 导致部分信息无法抓取.

 

使用方法:

  1. 通过bugreport命令抓取一份bugreport原始的文本log: shell# bugreport>/data/local/tmp/bugreport.txt
  2. 将上面命令抓取的bugreport.txt文本文件拷贝出来,上传到bugreport分析服务器上面,服务器会自动生成系统当前状态快照,上传方法如下: a. 通过浏览器进入服务器地址:http://10.27.254.108:8080/,如下图:

img

b. 点击broswer浏览report.txt文本文件,然后点击upload按钮上传文件,上传后,系统会自动生成系统状态快照,快照名称对应到上传时间,点击快照名称进入系统分析页面,如下:

img

该网页服务器的dockerfile如下:

附件: chkbugreport-server.zip

应用内存分析

android自带工具

dumpsys meminfo 应用内存

场景: 查看java进程的不同类别内存占用情况, 特别是确定activity是否泄露等信息.

命令 dumpsys meminfo com.china_liantong.launcher -d

输出:

Native Heap: native分配占用的RAM, 通过malloc申请的内存. Heap Size 是内存池总大小(向系统申请的内存大小). Heap Alloc 已经申请的内存大小(应用向dlmalloc申请的内存). Heap Free 内存池剩余可分配内存大小.

Dalvik Heap: 应用中Dalvi分配占用的RAM. Pss Total: 包含所有Zygote分配(通过进程间共享占用). Private Dirty: 仅分配到应用的实际RAM, 由应用分配或者zygote分配页, 这些页自从zygote fork应用进程后被修改过.

.so map.dex map: mmap的.so和.dex(Dalvik或ART)代码占用的RAM. Pss Total 数值包括应用之间共享的平台代码;Private Clean 是您的应用自己的代码。通常情况下,实际映射的内存更大 - 此处的 RAM 仅为应用执行的代码当前所需的 RAM。不过,.so mmap 具有较大的私有脏 RAM,因为在加载到其最终地址时对原生代码进行了修改(GOT?!)。

.oat mmap: 代码映像占用的 RAM 量,根据多个应用通常使用的预加载类计算。此文件在所有应用之间共享,不受特定应用影响。

.art mmap: 堆占用的 RAM 量,根据多个应用通常使用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响。尽管 ART 映像包含 Object 实例,它仍然不会计入您的堆大小。

Unknown: 系统无法将其分类到其他更具体的一个项中的任何 RAM 页。 其 Pss Total 与 Zygote共享.

TOTAL: 上方所有 PSS 字段的总和。表示您的进程占用的内存量占整体内存的比. Private DirtyPrivate Clean 是您的进程中的总分配,未与其他进程共享。它们(尤其是 Private Dirty)等于您的进程被破坏后将释放回系统中的 RAM 量。Dirty因为已被修改而必须保持在 RAM 中的 RAM 页(因为没有交换);Clean 是已从某个持久性文件(例如正在执行的代码)映射的 RAM 页,如果一段时间不用,可以移出分页。

ViewRootImpl: 进程中当前活动的根视图数量。每个根视图都与一个窗口关联,因此有助于确定涉及对话框或其他窗口的内存泄漏。

AppContextsActivities: 当前活动的应用 Context 和 Activity 对象数量。快速确定由于存在静态引用(比较常见)而无法进行垃圾回收的已泄漏 Activity 对象。这些对象经常拥有很多关联的其他分配,因此成为跟踪大型内存泄漏的一种不错的方式。

例子见内存泄露部分.

procmem

场景: 查看指定进程的各部分内存占用情况. 命令: procmem <pid> 例子: procmem 2509 数据来源: /proc/<pid>/maps/proc/<pid>/pagemap

img

各字段含义:

showmap

场景: 进程虚拟进程空间的内存分配情况 和 详细的干净脏数据信息.

/proc/[pid]/smaps 获取数据并统计的工具.

命令: showmap [-a] [-t] [-v] <pid> 例子: showmap -a 884 输出单位: KB

img

由于现在内核都使用了动态内存分配的机制, 在大量申请小内存的情况下, 从系统看内存有剩余, 但是进程却无法分配出需要的内存, 这就有可能是由于虚拟地址的内存碎片引起, 无法找到一段大于所请求内存大小的地址空间.

例子:

Android 应用 OOM

触发OOM的原因是, java层内存分配失败了.

造成内存分配失败的可能原因如下:

以上为网上的结论, 在android5.1.1上测试发现:

fd问题并不会导致OOM

代码复用下面内存泄露的代码,在asynctask中执行文件打开操作.

img

logcat中输出如下, 并没有OOM输出:

img

但是这段代码的确证明了打开文件太多会导致shmem创建失败.

内存泄露分析

activity 泄露

activity 的泄露基于android自带工具, 可以快速确定, 但是具体泄露的activity, 需要依赖工具 LeakCanary.

代码:

img

界面:

img

在第一次进入该应用时:

img

按下BUTTON, 然后退出应用:

img

第二次进入该应用:

img

第二次按下BUTTON:

img

第二次退出应用:

img

基于 dumpsys meminfo 确认应用有activity泄露后, 单纯通过UI操作基本也可以判断出来哪个activity泄露, 更准确的泄露检测工具是 LeakCanary, 该工具为第三方工具. 具体用法: https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/

还是以上的例子, 额外增加如下代码.

在MainActivity增加如下代码, 该代码的含义是, 在activity触发onDestroy时, 监控该activity对象.

增加application, 如下:

img

编译运行, 执行上面同样的操作. 在通知中心有如下显示:

img

点击后可以查看详细的堆栈:

img

同样在logcat中有如下输出:

img

StrictMode检测应用内存泄露

同样是上面的例子, 修改代码如下, 增加strickmode的调用:

img

同样的上面操作方法, logcat中有如下输出:

img

基于libc调试内存泄露

内存泄露的检测依据是, 进程退出时, 还有未释放的内存. 所以, valgrind包括bionic libc中, 能精确检测到泄露的前提条件就是进程退出. 但是这个对于系统级别的进程是无用的, 因为系统级进程并不会退出. 从RD角度, 判断系统级进程是否泄露的依据是内存是否持续增长, 特别是在压力测试的情况下. 所以, 这个部分介绍的工具, 实际上是抓取了持续增长的内存信息, 这些信息, 可能是内存泄露, 也可能是进程正常申请的内存, 需要RD介入判断.

附件: Memleak_tools_android.zip

依赖工具:

局限:

用法:

  1. 将libmemleakutils.so库链接到被调试程序中, 调用函数start_memleak_watcher(memstep), 其中memstep表示, pss每增加memstep, 则抓取一次当前内存分配信息.
  2. 将libmemleakutils.so 和 libc_malloc_debug_leak.so push到/system/lib/下.
  3. 将被调试程序push到 /system/bin/ 下.
  4. 设置 property libc.debug.malloc.program 为 你的调试进程名字.
  5. 设置 property libc.debug.malloc 为 1
  6. stop 或者 kill 被调试进程.
  7. start 或者 启动 被调试进程.
  8. 压力测试后, 检查 /data/local/tmp/ 底下存在文件 mem_snapshot_$pid_$memsize.log, 且个数大于2个(越多越好).
  9. cp /proc/${pid}/maps /data/local/tmp/
  10. 将 /data/local/tmp/ 底下所有文件复制到host机器.
  11. 用 diff 命令比较 两个 mem_snapshot_$pid_$memsize.log, 并保存输出为 diff_$pid_$memsize1_$memsize2.log
  12. 执行 ./addr2func.py --root-dir=/remote/ANDROID/hdtv/ --maps-file=./maps --product=zx2000 diff_$pid_$memsize1_$memsize2.log > mem_stack_$pid_$memsize1_$memsize2.log, 该过程是将diff生成文件中的所有地址转换成库和堆栈信息.
  13. 执行 ./memincrease.py mem_stack_$pi_$memsize1_$memsize2.log incr_$pid_$memsize1_$memsize2.log, 该过程是对12生成的文件进行统计排序, 从大到小排序.

例子:

img

运行一会儿后, 在 /data/local/tmp/目录下存在文件:

其内容如下:

执行diff命令:

得到一个PSS从1M增长到2M和3M增长到4M的两个diff文件. 内容如下:

2c2
<  Total memory 6293072
---
>  Total memory 7342672
4,5c4,5
< size  1048576, dup    1, 0xb6ed36b2, 0xb6f4cd4e, 0xb6fc8492, 0xb6fc8390, 0xb6f4ce68, 0xb6fc8400
< size     1024, dup    1, 0xb6ed36b2, 0xb6f4cd4e, 0xb6fc84aa, 0xb6fc8394, 0xb6f4ce68, 0xb6fc8400
---
> size  1048576, dup    2, 0xb6ed36b2, 0xb6f4cd4e, 0xb6fc8492, 0xb6fc8390, 0xb6f4ce68, 0xb6fc8400
> size     1024, dup    2, 0xb6ed36b2, 0xb6f4cd4e, 0xb6fc84aa, 0xb6fc8394, 0xb6f4ce68, 0xb6fc8400

执行 addr2func.py :

# ./addr2func.py --root-dir=/remote/ANDROID/hdtv/ --maps-file=./maps --product=zx2000 diff_2158_3_4.log > mem_stack_2158_3_4.log

生成文件内容如下:

2c2                                                                                                                                                                              [0/59466]

<  Total memory 8392272

---

>  Total memory 9441872

4,5c4,5

< size  1048576, dup    3,
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:11
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:25 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?
< size     1024, dup    3,
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test1(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:17
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:26 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?
---

> size  1048576, dup    4,
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:11
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:25 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?
> size     1024, dup    4,
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test1(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:17
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:26 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?

所有的地址被转换成了队长. 最后执行 memincrease.py :

./memincrease.py mem_stack_2158_3_4.log incr_2158_3_4.log

生成的文件 incr_2158_3_4.log 内容如下:

Memory increase 1048576 bytes                   ### 内存增加了 1048576 bytes
98990365afa0d71d62949620cc7e3753f386e4e3
old size 1048576 count 3, total memory 3145728       ### 从 3145728
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:11
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:25 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?
98990365afa0d71d62949620cc7e3753f386e4e3
new size 1048576 count 4, total memory 4194304      ### 增加到 4194304
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:11
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:25 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?

Memory increase 1024 bytes
833f23c991cc75facaa41fb74f7466b91c27917b
old size 1024 count 3, total memory 3072
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test1(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:17
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:26 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?
833f23c991cc75facaa41fb74f7466b91c27917b
new size 1024 count 4, total memory 4096
    leak_malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_leak.cpp:315 (discriminator 1)
    malloc, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/malloc_debug_common.cpp:259
    mem_test1(), /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:17
    main, /home/werther/workspace/ANDROID/hdtv/pdk/memleak/main.cpp:26 (discriminator 1)
    __libc_init, /home/werther/workspace/ANDROID/hdtv/bionic/libc/bionic/libc_init_dynamic.cpp:113
    _start, main.cpp:?

基本上, 将最顶上的几个堆栈确认下是否内存泄露即可.

特别注意:

valgrind 调试内存泄露

场景: 对于可退出的进程进行调试比较合适, 可以对应用的native代码进行调试.

目前hdtv代码上的 valgrind 无法获取被调试进程的堆栈, 只能获取它自己库的堆栈, 存在问题. 暂时无法使用.

https://pengzhangdev.github.io/Android-memory-debug/?nsukey=51bo38V797T9bIZtnqae1pe8N7U6foNUBkZLeOFk4GZ%2BUoFEDmMOshrTrOwC%2Fy0HByIi8JSyOlkA3Vw3b86en5MvUkr3SZJGF7QJbiI9snbu75dlz%2BWeuAAwChVVVLNxoZwRGXnBKnKewo3tJvUmbwfrvDGckSrjS1g6YlXy0Xlf0BUycYDu31bufbBzxC3Y0TBwpMckSTWzEbvgztgebg%3D%3D