内存泄漏介绍
什么是内存泄漏
内存泄漏是指程序分配了一块内存(通常是动态分配的堆内存),但在不再需要这块内存的情况下未将其释放。内存泄漏会导致程序浪费系统内存资源,持续的内存泄漏还导致系统内存的逐渐耗尽,最终导致程序或系统崩溃。
内存泄漏和常驻内存区别
常驻内存(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 是一种调试本机内存问题的方法。 它可以帮助检测内存损坏、内存泄漏和释放后使用问题。
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(进程名)
参数介绍:
- backtrace: 开启堆栈记录。
- guard: 开启内存越界检测。
- leak_track: 程序在退出时,如有内存泄漏,而不产生abort。
- backtrace_dump_on_exit: 程序退出时dump堆栈和内存信息。
- backtrace_dump_prefix: dump文件存放的路径和文件名的开头字符。如本处生成的文件放在/sdcard/目录下,文件名开头为heap字样,注意指定的路径要有写权限。
- libc.debug.malloc.program: 用于设置检测的程序,不设置则检测所有的运行的程序。
执行待测试程序
- 离线程序
离线程序运行完成后会在 backtrace_dump_prefix 设定的路径下存储 dump 文件 - 在线程序
需要先停掉程序所在的进程,再重启该进程才会生效。
由于在线程序一般不会主动退出(如 camerahalserver),需要使用命令来主动触发 dump。
命令:kill -47 xxx(进程ID),注意多次触发新文件覆盖之前的文件。
当你的程序有内存泄漏问题的话,输出如下报告:
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来做对比,找出真正增长的部分。
libmemunreachable
简介
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
参数
- backtrace_size 只收集泄漏指定 size 大小的 backtrace
- backtrace_min_size=192 backtrace_max_size=320 收集泄漏 size 介于两者之间的backtrec
重启应用,执行 dumpsys -t 600 meminfo --unreachable [process].(自测没有 dump 出预期结果)。下面是一个带有内存问题的输出结果。
Unreachable memory24 bytes in 2 unreachable allocationsABI: 'arm64'24 bytes unreachable at 71d37787d0first 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 71d37797d0first 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
- bool LogUnreachableMemory(bool log_contents, size_t limit)
- bool NoLeaks()
C++ interface
- bool GetUnreachableMemory(UnreachableMemoryInfo& info, size_t limit = 100)
- std::string GetUnreachableMemoryString(bool log_contents = false, size_t limit = 100)
核心函数是 GetUnreachableMemory() 其他三个函数内部都会调用此函数。
在使用添加代码的方式打印时,需要在编译代码时需要将 libmemunreachable.so 添加到动态依赖,libmemunreachable.so 文件可以在手机 /system/lib64/libmemunreachable.so 获取。
例子:
以下是一个包含内存泄漏的例子,在f 函数中申请了x, y 两块内存,在函数返回前x 被释放,y 赋值后没有被释放。
#include "./memunreachable.h"#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>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 interfaceprintf("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
- Asan(AddressSanitizer) 适用于检测内存越界访问、缓冲区溢出、内存泄漏等问题。它是一个在编译时插入的工具;
- 运行时有一定的性能开销(约增加两倍),代码大小和内存均有额外开销;
- 需要重新编译程序,编译时添加 address 相关选项;
- 可用于 linux 和 android,但在 android 上逐步被 HwASan 取代;
- 需要刷与 ASan 兼容的 ROM;
- 不再受支持,即使有bug 也不会修复;
HWASan
- HWASan 利用硬件特性,适用于检测内存错误,类似于 ASan,但能够更高效地运行在一些支持硬件特性的平台上。
- 性能开销和 Asan 接近,但内存占用更小;
- 需要重新编译程序,编译时添加 hwaddress 相关选项
- 仅适用于 Android 10 及更高版本,AArch64 硬件;
- 需要刷与 HWASan 兼容的 ROM;
MTE
- MTE(Memory Tagging Extension) 使用硬件标签来检测内存错误,主要专注于检测内存越界访问。
- 提供了较低的性能开销,首次具备了线上部署的可能。
- 无需重新构建代码来检测堆错误(但需要重新构建代码来检测堆栈错误)
- Android 系统在 Arm v9 上开始支持,仅适用于64位应用/程序;
Heapprofd
- Heapprofd 是一个跟踪给定时间段内 Android 进程的堆分配和释放的工具。
- 可以借助 Perfetto 抓取,开发人员可以使用该工具调查内存问题(调用栈和内存分配)。
当开启连续 dump 后,开发者可以查看程序结束前内存占用是否合理,以检查是否有潜在内存泄漏问题。或者将待测试代码循环执行,比较每执行一次代码段后内存是否有增加,一次判断是否有内存泄漏。
Valgrind 中的Memcheck
- Memcheck 是 Valgrind 工具套件中的一个工具,用于检测 C 和 C++ 程序中的内存错误。
- 内存问题检测比较全面,但对性能影响比较大,耗时增加10x~20x,不适用对时间敏感的程序。
- 在 Ubuntu 上安装:
sudo apt-get install valgrind
使用 memcheck 的基本方法
- 编译程序时加上 –g 选项,编译优化选项建议选择 -O1;
- 使用 Valgrind 命令运行程序:valgrind --leak-check=yes myprog arg1 arg2valgrind 使用 --tools 来指定 debug 工具,而 Memcheck 是默认工具,可以省略 --tools=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 的工作原理。
参考链接
- 【内存】Android C/C++ 内存泄漏分析 unreachable
- 调试本地内存使用 | Android 开源项目 | Android Open Source Project
- 调试和减少内存错误 | Android NDK | Android Developers (google.cn)
- Malloc Debug (googlesource.com)
- Malloc Hooks (googlesource.com)
- libmemunreachable (googlesource.com)
- Memcheck: a memory error detector
- Heap profiler - Perfetto Tracing Docs