内存泄漏是指程序分配了一块内存(通常是动态分配的堆内存),但在不再需要这块内存的情况下未将其释放。内存泄漏会导致程序浪费系统内存资源,持续的内存泄漏还导致系统内存的逐渐耗尽,最终导致程序或系统崩溃。
常驻内存(Resident Set)是指进程在运行期间占用的内存大小,包括进程使用的代码、数据和其他资源。常驻内存是进程在运行期间一直驻留在内存中的部分,即使在进程不活动时也不会被释放。
常驻内存通常不会带来显著的负面影响。
下图是源码与 ELF(可执行可链接) 文件以及运行起来后内存布局的简易映射关系图。
程序中的初始化全局变量和局部静态变量被编译到 .data,未初始化的全局变量和局部静态变量编译后放在 .bss 段,代码主体和函数主题存放在 .text 段,ELF 文件内实际有很多段(参考《程序员的自我修养-链接,装载与库》第三章)。
当程序运行时,会将 ELF 文件加载到内存。不同的段会加载到内存布局中的不同位置,其中 heap 这部分就是程序员手动去动态申请和释放内存的部分。当程序员用 malloc 函数申请了一块内存,使用完之后却没有 free 它的时候,就会发生内存泄漏。内存泄漏得越多,进程中可以使用内存的空间就越少,时间长了就会导致系统响应慢,甚至程序崩溃。
在 Android 系统上通常可以用 dumpsys meminfo 命令查看进程的内存使用数据,重复 dump 后从数据的变化情况来大致判断是否有内存泄漏。
也可以借助python 或者其他一些工具将数据可视化方便查看数据变化趋势。
但这只能大致的给你展示数据变化的趋势,而非直接明白的告诉你是否发生了内存泄漏。因此我们需要更精确的工具来检测是否也有内存泄漏。
本节我们将依次介绍 Malloc Debug, libmemunreachable, Asan, HwASan, MTE, Heapprofd, Memcheck(Valigrind)
内存泄漏检测工具(https://source.android.com/docs/core/tests/debug/native-memory?hl=zh-cn)
Malloc Debug 是一种调试本机内存问题的方法。 它可以帮助检测内存损坏、内存泄漏和释放后使用问题。
Malloc Debug 通过对常规的 allocation 函数包装了一层来记录和分析内存的申请和释放。这些函数包括:malloc, free, calloc, realloc, posix_memalign, memalign, aligned_alloc, malloc_usable_size
运行程序前的设置
adb shell setprop libc.debug.malloc.options "\"backtrace guard leak_track backtrace_dump_on_exit backtrace_dump_prefix=/sdcard/heap"\"
adb shell setprop libc.debug.malloc.program xxx(进程名)
参数介绍:
执行待测试程序
E malloc_debug: +++ memtest leaked block of size 48 at 0x7a4a6a42e0 (leak 1 of 2)
E malloc_debug: Backtrace at time of allocation:
E malloc_debug: #00 pc 000000000004461c /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
E malloc_debug: #01 pc 00000000000c83e8 /apex/com.android.runtime/lib64/bionic/libc.so (__register_atfork+40)
E malloc_debug: #02 pc 000000000005460c /apex/com.android.runtime/lib64/bionic/libc.so
E malloc_debug: #03 pc 00000000000613a0 /apex/com.android.runtime/bin/linker64
E malloc_debug: #04 pc 0000000000061144 /apex/com.android.runtime/bin/linker64
E malloc_debug: #05 pc 0000000000061144 /apex/com.android.runtime/bin/linker64
E malloc_debug: #06 pc 00000000000d5f14 /apex/com.android.runtime/bin/linker64
E malloc_debug: #07 pc 00000000000d4e0c /apex/com.android.runtime/bin/linker64
E malloc_debug: #08 pc 0000000000064004 /apex/com.android.runtime/bin/linker64
E malloc_debug: +++ memtest leaked block of size 20 at 0x793a6ae9a0 (leak 2 of 2)
E malloc_debug: Backtrace at time of allocation:
E malloc_debug: #00 pc 000000000004461c /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
E malloc_debug: #01 pc 00000000000100b8 /data/local/tmp/memtest/memtest
E malloc_debug: #02 pc 00000000000546e8 /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+104)
E malloc_debug: Dumping to file: /sdcard/heap.19748.exit.txt
注意报告中并不是所有的 leak 都是真正的内存泄漏,有些可能是常驻内存,开发者需要自己判断。
还需注意dump 路径要有写权限
很多时候在线运行环境下so 是无符号的程序,我们需要解析 dump 文件定位代码行号
python3 native_heapdump_viewer.py --symbols ./symboldir/ ./heap.4169.exit.txt --html > memtest4169.html
–symbols 指定的是符号库/程序的路径,子目录的路径必须要在手机上的路径一致。比如可执行程序在手机里的路径是/vendor/bin/memtest,那解释时它的带符号的程序路径上需要是 ./symboldir/vendor/bin/memtest
检测出来的并不是都是泄漏,一部分是属于常驻内存,尤其对于在线程序,我们需要将程序运行不同的次数,抓出不同的log来做对比,找出真正增长的部分。
Android 的 libmemunreachable 是一个零开销的本地内存泄漏检测器。 它会在触发内存检测的时候遍历进程内存,同时将任何不可访问的块报告为泄漏。
设置属性
adb root
adb shell setprop libc.debug.malloc.program app_process
adb shell setprop wrap.[process] "\$\@“
adb shell setprop libc.debug.malloc.options backtrace=4
参数
重启应用,执行 dumpsys -t 600 meminfo --unreachable [process].(自测没有 dump 出预期结果)。下面是一个带有内存问题的输出结果。
Unreachable memory
24 bytes in 2 unreachable allocations
ABI: 'arm64'
24 bytes unreachable at 71d37787d0
first 20 bytes of contents:
71d37787d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
71d37787e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
24 bytes unreachable at 71d37797d0
first 20 bytes of contents:
71d37797d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
71d37797e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
官方提供4个接口来检测内存
C interface
C++ interface
核心函数是 GetUnreachableMemory() 其他三个函数内部都会调用此函数。
在使用添加代码的方式打印时,需要在编译代码时需要将 libmemunreachable.so 添加到动态依赖,libmemunreachable.so 文件可以在手机 /system/lib64/libmemunreachable.so 获取。
例子:
以下是一个包含内存泄漏的例子,在f 函数中申请了x, y 两块内存,在函数返回前x 被释放,y 赋值后没有被释放。
#include "./memunreachable.h"
#include
#include
#include
#include
using namespace android;
void f(void);
void f(void) {
printf("[memtest] function f\n");
int* x = (int*)malloc(10 * sizeof(int));
x[0] = 0;
int* y = (int*)malloc(5 * sizeof(int));
y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;
y[4] = 4;
free(x);
}
int main(void) {
printf("[memtest] hello main\n");
f();
// C interface
printf("LogUnreachableMemory()\n");
LogUnreachableMemory(true, 100);
return 0;
}
adb log 输出(考虑排版省去时间戳)
log 里显示有一个 20 bytes 的内存泄漏,20 正是5 个 int 的大小,对应申请但没有释放的 y 地址的内存。
// 新建 Collection process
31232 31231 I libmemunreachable: collecting thread info for process 31231...
31232 31231 I libmemunreachable: collection thread done
// fork 进程运行 sweeping process
31233 31233 I libmemunreachable: searching process 31231 for allocations
31233 31233 I libmemunreachable: searching done
31233 31233 I libmemunreachable: sweeping process 31231 for unreachable memory
31233 31233 I libmemunreachable: sweeping done
31233 31233 I libmemunreachable: folding related leaks
31233 31233 I libmemunreachable: folding done
// 回到 Original process 接收检测结果
31231 31231 I libmemunreachable: unreachable memory detection done
31231 31231 E libmemunreachable: 20 bytes in 1 allocation unreachable out of 1260 bytes in 7 allocations
31231 31231 E libmemunreachable: 20 bytes unreachable at 7a03454400
31231 31231 E libmemunreachable: contents:
31231 31231 E libmemunreachable: 7a03454400: 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 ................
31231 31231 E libmemunreachable: 7a03454410: 04 00 00 00 ....
31231 31231 E libmemunreachable: #00 pc 000000000003e238 /apex/com.android.runtime/lib64/bionic/libc.so (malloc+84)
31231 31231 E libmemunreachable: #01 pc 00000000000100b8 /data/local/tmp/memtest/memtest_libmemunreachable
31231 31231 E libmemunreachable: #02 pc 000000000004aa48 /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+100)
调用 bool LogUnreachableMemory(bool log_contents, size_t limit) 时,log_contents 传 true 会打印泄露地址的内容,也就是 contents 对应的两行内容。之后可以用 address2line 解析行号。
ASan
HWASan
MTE
Heapprofd
Valgrind 中的Memcheck
sudo apt-get install valgrind
使用 memcheck 的基本方法
$ valgrind --leak-check=yes ./memtest_origin
==19517== Memcheck, a memory error detector
==19517== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==19517== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==19517== Command: ./memtest_origin
==19517==
[memtest] hello main
[memtest] function f
==19517==
==19517== HEAP SUMMARY:
==19517== in use at exit: 20 bytes in 1 blocks
==19517== total heap usage: 3 allocs, 2 frees, 1,084 bytes allocated
==19517==
==19517== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1
==19517== at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==19517== by 0x1086FF: f() (memtest_origin.cc:9)
==19517== by 0x108731: main (memtest_origin.cc:16)
==19517==
==19517== LEAK SUMMARY:
==19517== definitely lost: 20 bytes in 1 blocks
==19517== indirectly lost: 0 bytes in 0 blocks
==19517== possibly lost: 0 bytes in 0 blocks
==19517== still reachable: 0 bytes in 0 blocks
==19517== suppressed: 0 bytes in 0 blocks
==19517==
==19517== For counts of detected and suppressed errors, rerun with: -v
==19517== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
本文我们介绍了内存泄漏的概念,malloc debug 和 libmemunreachable 的使用方法,以及一些其他内存检测工具的简介,下一篇我们将介绍 malloc debug 和 libmemunreachable 的工作原理。