android 如何分析应用的内存(十)
接下来介绍native heap内存的第四个板块————malloc统计和libmemunreachable
malloc统计
malloc统计是标准c库提供的接口。他有两个调用接口如下:
#include <malloc.h>
struct mallinfo mallinfo(void);
struct mallinfo2 mallinfo2(void);
他们返回内存分配相关的统计信息。其中mallinfo2结构体和mallinfo结构体字段一样,区别仅是:mallinfo结构体的字段类型为int,而mallinfo2结构体的字段类型为size_t.
int类型会导致某些情况下长度不够。因此应该使用mallinfo2函数。
需要注意的是,mallinfo2和mallinfo统计的是malloc函数,或者相关函数的内存分配情况。
对于其他非标方法获得的内存,无法通过这两个函数统计到。此时可以使用如下的函数
// 将所有的内存统计,以xml格式,输出到stream中
int malloc_info(int options, FILE *stream);
mallinfo2结构体解释:
struct mallinfo2 {
size_t arena; /* 总的分配字节数(不包含映射区域),包含空闲和非空闲 */
size_t ordblks; /* 空闲块数 */
size_t smblks; /* fastbin块数(这是一种小内存块) */
size_t hblks; /* mmap分配的块数 */
size_t hblkhd; /* mmap分配的字节数 */
size_t usmblks; /* 未被使用,总是为0 */
size_t fsmblks; /* fastbin 空闲块中的字节总数 */
size_t uordblks; /* 正在使用的分配的字节总数 */
size_t fordblks; /* 空闲块的字节数 */
size_t keepcost; /* 堆顶可释放空间的总量。理想情况下(即忽略页面对齐限制等),
这是通过 malloc_trim 可以释放的字节的最大数量。 */
};
注意:应用程序使用c库的函数,请求内存分配,返回的是由c库维护的内存池。因此内存池就会被分成空闲和非空闲两种。
注意:fastbin块,可以叫做“快速块”。它是c库维护的一种特殊内存块,它比较小。当调用malloc分配小块内存时,就从fastbin中分配内存,而不是从常规内存池中分配。这样有助于减少内存碎片
上面结构体注释中提及的malloc_trim函数,可以将c库维护的内存的顶端部分,返还给操作系统,以缓解内存紧张。而keepcost就是能够返回的最大值。
mallinfo2例子
先使用mallocinfo2函数,获取当前的内存统计信息。然后分配一段内存。再次使用mallocinfo2统计信息.
先定义一个函数printMallocInfo。该函数获取当前的内存统计信息,并打印:
void printMallocInfo(){
struct mallinfo2 info = mallinfo2();
ALOGD("总的字节数(空闲和非空闲)%zu",info.arena);
ALOGD("空闲块数 %zu",info.ordblks);
ALOGD("fastbin块数 %zu",info.smblks);
ALOGD("mmap块数 %zu",info.hblks);
ALOGD("mmap字节数 %zu",info.hblkhd);
ALOGD("fastbin空闲字节数 %zu",info.fsmblks);
ALOGD("已经分配字节数 %zu",info.uordblks);
ALOGD("空闲块字节数 %zu",info.fordblks);
ALOGD("堆顶可释放总量 %zu",info.keepcost);
}
然后分配一段内存,并再次打印,如下:
printMallocInfo();
//为了防止使用到fastbin中的内存块,因此一次分配1024个字节
for(int i=0;i<100;++i){
//分配1024个字节
unsigned char * testChar = new unsigned char[1024];
for(int i=0;i<1024;++i){
testChar[i] = '0xa';
}
testChar[1023] = '\0';
ALOGD("testChar %s",testChar);
}
printMallocInfo();
在Android的log系统重,检查输出值,如下:
从图中可以看到,分配前后,“已经分配字节数”相差102400.正是我们分配的大小。
但是在上面的截图中,有很多项一直是0,这是因为Android的libc库,目前只统计了
“已经分配的字节数”和“空闲块数”。那么其他的内存情况怎么查看呢?可以通过malloc_info进行查看
malloc_info例子
先定义malloc_info的调用函数如下:
void printMallocInfoToFile(){
FILE * file = fopen("/sdcard/memroy.xml", "w");
if(malloc_info(0,file) == 0 ) {
ALOGD("malloc_info succeed");
}else{
ALOGD("malloc_info error %d ,%s",errno, strerror(errno));
}
fclose(file);
}
它会将相应的详细信息,输出到/sdcard/memroy.xml文件中。
注意:对于Android系统而言,要访问文件,需要使用对应的权限。
然后在合适的地方,调用上面的函数,最终,我们将看到如下的xml文件。
<malloc version="jemalloc-1">
<heap nr="0">
<allocated-large>3223552</allocated-large>
<allocated-huge>4194304</allocated-huge>
<allocated-bins>2996248</allocated-bins>
<bin nr="0">
<allocated>3992</allocated>
<nmalloc>798</nmalloc>
<ndalloc>299</ndalloc>
</bin>
<bin nr="1">
<allocated>14816</allocated>
<nmalloc>962</nmalloc>
<ndalloc>36</ndalloc>
</bin>
<!--省略若干-->
<bin nr="35">
<allocated>200704</allocated>
<nmalloc>26</nmalloc>
<ndalloc>12</ndalloc>
</bin>
<bins-total>2996248</bins-total>
</heap>
<heap nr="1">
<allocated-large>0</allocated-large>
<allocated-huge>0</allocated-huge>
<allocated-bins>269616</allocated-bins>
<bin nr="0">
<allocated>64</allocated>
<nmalloc>8</nmalloc>
<ndalloc>0</ndalloc>
</bin>
<bin nr="1">
<allocated>176</allocated>
<nmalloc>12</nmalloc>
<ndalloc>1</ndalloc>
</bin>
<!--省略若干-->
<bin nr="28">
<allocated>40960</allocated>
<nmalloc>10</nmalloc>
<ndalloc>0</ndalloc>
</bin>
<bins-total>269616</bins-total>
</heap>
</malloc>
下面是一个解释:
<malloc version="jemalloc-1"> :jemalloc分配器的版本
<heap nr="0">:第几号堆。为了保持线程的安全,每个线程有一个对应的堆。因此一个应用中可以
有多个heap
<allocated-large>3223552</allocated-large>:已经分配的大内存字节数。如大数组,
3D模型等
<allocated-huge>4194304</allocated-huge>:已经分配的巨大内存字节数。
<allocated-bins>2996248</allocated-bins>:已经分配的bins的字节数。bins是jemalloc
使用的一种数据结构,这种数据结构,可以方便的管理heap中的内存。
事实上,查看heap的统计信息,只要前面三个就可以了。下面的bin是更加细致的描述。
<bin nr="0">
<allocated>3992</allocated>
<nmalloc>798</nmalloc>
<ndalloc>299</ndalloc>
</bin>
nr:表示bin的编号
allocated:表示bin中已经分配的内存字节数
nmalloc和ndalloc:分别表示malloc和dalloc的调用次数。如果在一个运行周期内:
dalloc次数远大于nmalloc,则可能存在内存泄漏
上面介绍的malloc统计信息,可以作用在时间轴上,可以观测heap的变化情况,而判断是否有内存泄漏。
一旦发现内存泄漏之后,可以使用libmemunreachable来查看具体的泄漏点。
libmemunreachable
libmemunreable是Android真正意义上的内存泄漏检测器。它在native层实现了类似于JVM的垃圾回收器,会去遍历所有的native heap内存,从而判断哪些内存不可达,即内存泄漏。
libmemunreachable 原理简述
现将整个流程简述如下:
- 被检测的某个线程,触发内存检测。(通过调用相应的函数,见后文)
- Android创建一个收集进程。这个收集进程和被检查的进程共享地址空间。
- 收集进程使用ptrace,控制被检查进程暂停所有的线程
- 收集进程,收集被检测进程的寄存器内容,堆栈内容,以及内存映射等数据
- 收集完成,然后创建,一个sweeper进程。该进程使用收集进程收集的各种数据,遍历内存
- 在遍历内存的时候,收集进程使用ptrace,再次运行被检测进程,然后收集进程退出
- 被检测进程继续运行,但触发内存泄漏的线程,停在原地,等待sweeper进程的分析结果。
如何启用libmemunreachable
第一步:为了能够打印更多的信息,需要按照malloc调试的方法,打开对应的开关,下面是在wrap.sh中的例子:
#!/system/bin/sh
export LIBC_DEBUG_MALLOC_OPTIONS=backtrace
exec "$@"
注意:一定要打开 useLegacyPackaging否则出现apk无法安装的情况
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
同样也可以直接在adb shell中操作如下:
adb root
adb shell setprop libc.debug.malloc.program app_process
adb shell setprop wrap.[process] "\$\@"
adb shell setprop libc.debug.malloc.options backtrace=16
注意:当打开malloc debug之后,会变得比较卡。为了能够跟踪特定的内存。可以使用
backtrace_size backtrace_min_size backtrace_max_size三个开关。他们可以限定跟踪的内存大小。详细说明见android 如何分析应用的内存(八)
第二步:有了上面的准备。在需要进行内存检测的地方,调用如下的函数。
//c接口
//将memory leak输出到log系统。limit表示最多有多少memory leak输出到log中。
//log_content表示memory leak前面是否有32字节的内容
// 如果调用成功,返回true
bool LogUnreachableMemory(bool log_contents, size_t limit);
//如果没有未达的内存,则返回true
bool NoLeaks();
//cpp接口
//暂时不做介绍,因为在我的环境中,没有跑通,可参阅:https://android.googlesource.com/platform/system/memory/libmemunreachable/+/master/README.md
bool GetUnreachableMemory(UnreachableMemoryInfo& info, size_t limit = 100);
std::string GetUnreachableMemoryString(bool log_contents = false, size_t limit = 100);
从如下地址中,获取对应的头文件:libmemunreachable
然后放入合适位置,如下:
第三步:将目标平台的相应的so库,pull出来,放入libs目录,如下:
然后添加相应的链接路径如下:
set(LINK_FLAGS "-lmemunreachable -L/Users/biaowan/AndroidStudioProjects/Test_Malloc.old/app/libs/arm64-v8a")
set(CMAKE_SHARED_LINKER_FLAGS "${LINK_FLAGS}")
注意:对于应用开发者而言,一定要从对应平台中pull出相应的库。如,在Android 8.1上面运行测试,就pull Android 8.1的。
同时还要注意的是,图中有两个libc++.so和libc++_shared.so。他们存在相同的部分,因此当出现类似“can’t located symbolxxx”的错误时,可以相互更换名字试试。在我的例子中,libc++_shared.so和libc++.so均来自于ndk 25.2.9519653
注意:如果是Framework工程师,则无需这么繁琐,按照正常的编译流程即可
第四步:一旦运行成功,则会出现如下的内容
如上图,系统会将内存泄漏的详细堆栈打印出来。
至此,mallocinfo和libmemunreachable介绍完毕。
下一篇文章是heap分析的第五个板块:使用HWASan/Asan工具,查找内存错误