后端开发日记 - 检查内存泄漏

内存泄漏一般是由于程序在堆上分配了内存而没有释放,随着程序的运行占用的内存越来越大,一方面会影响程序的稳定性,可能让运行速度越来越慢,或者造成 OOM,甚至会影响程序所运行的机器的稳定性,造成宕机。

而分析内存问题的常用工具有 valgrindgperftools 等,当然也可以自行开发钩子函数,本文主要介绍 gperftools 来进行内存泄漏的分析。

安装 gperftools

首先是一些依赖环境的安装:

## libunwind 64 位系统必选
git clone https://github.com/libunwind/libunwind.git
cd libunwind
sh ./autogen.sh
./configure
make
sudo make install

## 生成 PDF 可视化必选
sudo apt-get install graphviz graphviz-doc
sudo yum install graphviz graphviz-doc
sudo yum install ghostscript

然后就可以安装 gperftools 工具了:

git clone https://github.com/gperftools/gperftools.git
./autogen.sh
./configure
make
sudo make install

安装完成后,如果程序运行环境与编译环境不是同一台主机,需要拷贝出一些成果物放置到合适的位置,主要成果物包括:

  1. lib 文件,需要放在运行环境中
    • libunwind.so.8
    • libtcmalloc.so.4
  2. 头文件,需要放在代码中方便集中管理
    • heap-checker.h
    • heap-profiler.h
  3. 工具文件,可以放在编译环境中使用,也可以拷贝到其他主机上使用
    • pprof

使用 gperftools

gperftools 提供了 4 个工具:

  • thread-caching malloc : 简称 tcmalloc,可以用来替代 glibc 中原有的 malloc/freenew/delete 等函数
  • heap-checking using tcmalloc : 用来检查程序中的内存泄漏位置,适用于 C++
  • heap-profiling using tcmalloc : 用来统计程序中的内存申请、释放情况,可用于检查内存使用情况和泄漏情况,适用于 C/C++,可应用于所有可执行文件
  • CPU profiler : 用来统计程序中每个部分占用 CPU 性能情况,用于程序 CPU 性能的观察和优化

以上 4 个工具中,用于分析内存泄漏的有两个工具:heap-checking using tcmallocheap-profiling using tcmalloc

使用 heap-checking

使用 heap-checking 有两种方式,一种是设置环境变量的方法,一种是修改代码的方法

设置环境变量来使用 heap-checking

env LD_PRELOAD="/usr/lib/libtcmalloc.so"
## 假设我们要检查的程序是 /usr/local/bin/my_binary_compiled_with_tcmalloc
env HEAPCHECK=normal /usr/local/bin/my_binary_compiled_with_tcmalloc
  1. minimal
  2. normal
  3. strict
  4. draconian

"minimal":堆检查在初始化中尽可能晚地开始,这意味着您可以在初始化例程中泄漏一些内存,并且不会触发泄漏消息。如果您经常在一次全局初始化中故意泄漏数据,则 "minimal" 模式对您非常有用。否则,应使用更严格的模式。

"normal" 堆检查跟踪活动对象,并报告程序退出时无法通过活动对象访问的任何数据的泄漏,是谷歌最常用的模式,适用于日常堆检查使用。

"strict" 堆检查很像 "normal",但有一些额外的检查,即内存不会丢失在全局析构函数中。特别是,如果您有一个全局变量,该变量在程序执行期间分配内存,然后在全局析构函数中 "forgets" 内存(例如,将指针设置为 NULL),这将在 "strict" 模式下提示泄漏消息,而在 "normal" 模式下并不会进行提示。

"draconian" 堆检查适合那些喜欢非常精确地了解其内存管理,并且希望堆检查器帮助他们实施它的人。在 "draconian" 模式下,堆检查器不会执行 "live object" 检查,因此除非在程序退出之前释放了所有分配的内存,它都会报告泄漏。

修改代码来使用 heap-checking

可参考:https://gperftools.github.io/gperftools/heap_checker.html

分析 heap-checking 输出

使用 heap-profiling

heap-checking 一样,heap-profiling 也有同样的两种方法来使用

设置环境变量来使用 heap-profiling

env LD_PRELOAD="/usr/lib/libtcmalloc.so"
## 假设我们要检查的程序是 /usr/local/bin/my_binary_compiled_with_tcmalloc
env HEAPPROFILE=/tmp/mybin.hprof /usr/local/bin/my_binary_compiled_with_tcmalloc

除了以上环境变量,还有一些环境变量可以设置:

环境变量 默认值 说明
HEAP_PROFILE_ALLOCATION_INTERVAL 1073741824 (1GB) 每次程序分配指定字节数时,转储堆分析信息。
HEAP_PROFILE_INUSE_INTERVAL 104857600(100M) 每当高水位内存使用标记增加指定字节数时,转储堆分析信息。
HEAP_PROFILE_TIME_INTERVAL 0 每次经过指定的秒数时转储堆分析信息。
HEAPPROFILESIGNAL 已禁用 每当将指定的信号发送到进程时,转储堆分析信息。

修改代码来使用 heap-profiling

#include <gperftools/heap-profiler.h>
/* 启动堆分析器 */
HeapProfilerStart()
/* 停止堆分析器 */
HeapProfilerStop()
/* 转储堆分析器分析结果 */
HeapProfilerDump()
GetHeapProfile()
/* 检查堆分析器是否启动 */
IsHeapProfilerRunning()

示例程序:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <gperftools/heap-profiler.h>
#define WAIT_FOREVER(t) do { while(1) { sleep(t); } } while(0)

void* leek_malloc(void* arg) {
    char* ptr = NULL;
    while (1) {
        ptr = malloc(1024); /* 1024 Byte */
        printf("malloc 1024 byte success.\n");
        if (time(NULL) % 2 == 0) free(ptr);
        sleep(1);
    }
    return NULL;
}

void* leek_check(void* arg) {
    while (1) {
        HeapProfilerDump("check");
        sleep(5);
    }
    return NULL;
}

int main(int argc, char* argv[]) {
    pthread_t tidmalloc;
    pthread_t tiddump;
    HeapProfilerStart("start");

    pthread_create(&tidmalloc, NULL, leek_malloc, NULL);
    pthread_create(&tidmalloc, NULL, leek_check, NULL);

    WAIT_FOREVER(10);
    HeapProfilerStop();
    return 0;
}

分析 heap-profiling 输出

如果在程序中打开堆分析,程序将定期将配置文件写入文件系统。配置文件序列将命名为:

    <prefix>.0000.heap
    <prefix>.0001.heap
    <prefix>.0002.heap
    ...

<prefix> 是运行代码时提供的文件名前缀(或者通过环境变量 HEAPPROFILE 提供)的位置。请注意,如果提供的前缀不是以 '/' 开头,则配置文件将写入程序的工作目录。

通过将配置文件输出传递到工具 pprof 可以查看配置文件输出。

参考资料