android 如何分析应用的内存(十)——malloc统计和libmemunreachable

android 如何分析应用的内存(十)

接下来介绍native heap内存的第四个板块————malloc统计和libmemunreachable

malloc统计

malloc统计是标准c库提供的接口。他有两个调用接口如下:

#include 

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系统重,检查输出值,如下:
android 如何分析应用的内存(十)——malloc统计和libmemunreachable_第1张图片

从图中可以看到,分配前后,“已经分配字节数”相差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>3223552allocated-large>
        <allocated-huge>4194304allocated-huge>
        <allocated-bins>2996248allocated-bins>
        <bin nr="0">
            <allocated>3992allocated>
            <nmalloc>798nmalloc>
            <ndalloc>299ndalloc>
        bin>
        <bin nr="1">
            <allocated>14816allocated>
            <nmalloc>962nmalloc>
            <ndalloc>36ndalloc>
        bin>
        
        <bin nr="35">
            <allocated>200704allocated>
            <nmalloc>26nmalloc>
            <ndalloc>12ndalloc>
        bin>
        <bins-total>2996248bins-total>
    heap>
    <heap nr="1">
        <allocated-large>0allocated-large>
        <allocated-huge>0allocated-huge>
        <allocated-bins>269616allocated-bins>
        <bin nr="0">
            <allocated>64allocated>
            <nmalloc>8nmalloc>
            <ndalloc>0ndalloc>
        bin>
        <bin nr="1">
            <allocated>176allocated>
            <nmalloc>12nmalloc>
            <ndalloc>1ndalloc>
        bin>
        
        <bin nr="28">
            <allocated>40960allocated>
            <nmalloc>10nmalloc>
            <ndalloc>0ndalloc>
        bin>
        <bins-total>269616bins-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 原理简述

现将整个流程简述如下:

  1. 被检测的某个线程,触发内存检测。(通过调用相应的函数,见后文)
  2. Android创建一个收集进程。这个收集进程和被检查的进程共享地址空间。
  3. 收集进程使用ptrace,控制被检查进程暂停所有的线程
  4. 收集进程,收集被检测进程的寄存器内容,堆栈内容,以及内存映射等数据
  5. 收集完成,然后创建,一个sweeper进程。该进程使用收集进程收集的各种数据,遍历内存
  6. 在遍历内存的时候,收集进程使用ptrace,再次运行被检测进程,然后收集进程退出
  7. 被检测进程继续运行,但触发内存泄漏的线程,停在原地,等待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

然后放入合适位置,如下:
android 如何分析应用的内存(十)——malloc统计和libmemunreachable_第2张图片

第三步:将目标平台的相应的so库,pull出来,放入libs目录,如下:
android 如何分析应用的内存(十)——malloc统计和libmemunreachable_第3张图片

然后添加相应的链接路径如下:

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工程师,则无需这么繁琐,按照正常的编译流程即可

第四步:一旦运行成功,则会出现如下的内容
android 如何分析应用的内存(十)——malloc统计和libmemunreachable_第4张图片

如上图,系统会将内存泄漏的详细堆栈打印出来。

至此,mallocinfo和libmemunreachable介绍完毕。

下一篇文章是heap分析的第五个板块:使用HWASan/Asan工具,查找内存错误

你可能感兴趣的:(android,内存分析,mallocinfo,malloc_info,memunreachable,内存泄漏,打印内存泄漏)