后端开发日记 - 检查内存泄漏
内存泄漏一般是由于程序在堆上分配了内存而没有释放,随着程序的运行占用的内存越来越大,一方面会影响程序的稳定性,可能让运行速度越来越慢,或者造成 OOM
,甚至会影响程序所运行的机器的稳定性,造成宕机。
而分析内存问题的常用工具有 valgrind
和 gperftools
等,当然也可以自行开发钩子函数,本文主要介绍 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
安装完成后,如果程序运行环境与编译环境不是同一台主机,需要拷贝出一些成果物放置到合适的位置,主要成果物包括:
lib
文件,需要放在运行环境中libunwind.so.8
libtcmalloc.so.4
- 头文件,需要放在代码中方便集中管理
heap-checker.h
heap-profiler.h
- 工具文件,可以放在编译环境中使用,也可以拷贝到其他主机上使用
pprof
使用 gperftools
gperftools
提供了 4
个工具:
thread-caching malloc
: 简称tcmalloc
,可以用来替代glibc
中原有的malloc/free
、new/delete
等函数heap-checking using tcmalloc
: 用来检查程序中的内存泄漏位置,适用于C++
heap-profiling using tcmalloc
: 用来统计程序中的内存申请、释放情况,可用于检查内存使用情况和泄漏情况,适用于C/C++
,可应用于所有可执行文件CPU profiler
: 用来统计程序中每个部分占用 CPU 性能情况,用于程序 CPU 性能的观察和优化
以上 4
个工具中,用于分析内存泄漏的有两个工具:heap-checking using tcmalloc
和 heap-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
- minimal
- normal
- strict
- 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
可以查看配置文件输出。