0%

使用 gperftools 检测内存泄露

每个 C/C++ 程序员可能都经历过定位内存泄露问题的痛苦。一方面,为了减少内存泄露问题,在 C++ 程序中,我们应该尽量使用智能指针,在 C 程序中,我们也可以通过一些内存池技术来管理内存的申请与释放。另一方面,当内存泄露问题真的出现时,通过 gperftools 的 heap checker,我们也可以比较轻松地找到内存泄露的线索,缩短问题定位时间。

gperftools 简介

gperftools 是 google 开源的一组套件,提供了高性能的、支持多线程的 malloc 实现,以及一组优秀的性能分析工具。

安装 gperftools

接下来介绍如何从源码安装 gperftools:

下载源码

从 gperftools github 官网上下载最新版本的源码包:

1
$ wget https://github.com/gperftools/gperftools/releases/download/gperftools-2.9.1/gperftools-2.9.1.tar.gz

解压源码包

1
$ tar -zxv -f gperftools-2.9.1.tar.gz

configure

1
2
$ cd gperftools-2.9.1
$ ./configure

命令结束执行后出现一个报错:

1
configure: WARNING: No frame pointers and no libunwind. Using experimental backtrace capturing via libgcc. Expect crashy cpu profiler.

这是因为没有安装 libunwind。这里直接使用 yum 的方式安装:

1
yum install libunwind-devel

再次执行 ./configure,命令执行成功。

编译并安装

执行如下两个命令,进行编译并安装:

1
2
make
sudo make install

最后执行 ldconfig 更新动态库文件

确认安装成功

执行如下命令,确认 gperftools 安装成功

1
2
3
4
5
6
7
8
9
[root@36eab106d3bf gperftools-2.9.1]# pprof --version
pprof (part of gperftools 2.0)

Copyright 1998-2007 Google Inc.

This is BSD licensed software; see the source for copying conditions
and license information.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

内存泄露检测

gperftools 的 heap checker 组件可以用于检测 C++ 程序中的内存泄露问题。要使用 heap checker,总共分 3 步:

  • 链接 tcmalloc 库到应用程序中
  • 运行程序
  • 分析输出

链接 tcmalloc 库

heapchecker 是 tcmalloc 的一部分,所以为了在可执行程序中使用 heap checker,在应用程序的链接阶段使用 -ltcmalloc 链接 tcmalloc 库。

运行代码

使用 heap checker 的推荐方式是 完整程序运行模式,此时 heap checker 可以在程序的 main 函数开始前跟踪内存分配,然后在程序退出时再次检查。如果它发现来了任何内存泄露,heap checker 会直接终止程序(通过 exit(1)),并打印一条消息,告诉你接下来如何使用 pprof 来继续跟踪该内存泄露问题。

完整程序运行 的 heap checker 支持 4 种模式:

  • minimal:在程序初始化期间(即 main 函数运行前)不进行内存泄露检查
  • normal:正常模式,通过跟踪某块内存是否可以被 live object 来访问,来判断是否出现内存泄露
  • strict:类似于 normal 模式,但是对全局对象的内存泄露有一些额外的检查
  • draconian:在该模式下,只有所有申请的内存都被释放,才认为没有出现内存泄露

一般使用 normal 模式就可以满足日常要求了。

heap checker 的另一种使用方法是检测指定代码块是否出现了内存泄露。为了实现这一点,需要在代码片段的开始部分创建一个 HeapLeakChecker 结构体,并在结束部分调用 NoLeaks()。例如:

1
2
3
4
5
6
HeapLeakChecker heap_checker("test_foo");
{
code that exercises some foo functionality;
this code should not leak memory;
}
if (!heap_checker.NoLeaks()) assert(NULL == "heap memory leak");

需要注意,添加 HeapLeakChecker 只是在程序中添加了内存泄露检测代码,为了真实地检测程序是否出现了内存泄露,仍然需要运行程序,并打开 heap-checker。

1
env HEAPCHECK=local your_program

除了指定为 local 模式外,之前的 normal 等模式也是可以的,此时除了运行 local 检查外,还将进行 完整程序运行 检查。

当然,运行 heap checker 是有代价的。heap checker 需要记录每次内存申请时的调用栈信息,这就导致了使用 heap checker 时,程序需要消耗更多的内存,同时程序运行速度也更慢。另外,需要注意,由于 heap checker 内部使用了 heap profile 框架,所以不能同时运行 heap checker 和 heap profile。

忽略已知的内存泄露

对于已知的内存泄露,如果想让 heap checker 忽略这些内存泄露信息,可以在应用程序代码中添加中如下代码:

1
2
3
4
{
HeapLeakChecker::Disabler disabler;
<leaky code>
}

另一种方式是使用 IgnoreObject,它接收一个指针参数,对该参数所指向的对象将不再进行内存泄露检查。

使用 pprof 查看内存泄露结果

heap checker 运行结束时会打印基本的泄露信息,包括调用栈和泄露对象的地址。除此之外,还可以使用 pprof 命令行工具来可视化地查看调用栈。

示例

接下来通过一个示例讲述如何使用 gperftools 的 heap checker 来发现程序的内存泄露问题。

  1. 示例程序如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Copyright (C) fuchencong.com

#include <iostream>

int func() {
int *p = new int(10);
return 0;
}

int main() {
std::cout << "memory leak test" << std::endl;
return func();
}
  1. 编译程序,并链接 tcmalloc 库:
1
$ g++ -std=c++0x -g -o memory_leak memory_leak.cpp -ltcmalloc
  1. 运行程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@36eab106d3bf gperftools-test]# env HEAPCHECK=normal ./memory_leak
WARNING: Perftools heap leak checker is active -- Performance may suffer
memory leak test
Have memory regions w/o callers: might report false leaks
Leak check _main_ detected leaks of 4 bytes in 1 objects
The 1 largest leaks:
*** WARNING: Cannot convert addresses to symbols in output below.
*** Reason: Cannot find 'pprof' (is PPROF_PATH set correctly?)
*** If you cannot fix this, try running pprof directly.
Leak of 4 bytes in 1 objects allocated from:
@ 4008ff
@ 400935
@ 7fc9f81a6555
@ 400829


If the preceding stack traces are not enough to find the leaks, try running THIS shell command:

pprof ./memory_leak "/tmp/memory_leak.16582._main_-end.heap" --inuse_objects --lines --heapcheck --edgefraction=1e-10 --nodefraction=1e-10 --gv

If you are still puzzled about why the leaks are there, try rerunning this program with HEAP_CHECK_TEST_POINTER_ALIGNMENT=1 and/or with HEAP_CHECK_MAX_POINTER_OFFSET=-1
If the leak report occurs in a small fraction of runs, try running with TCMALLOC_MAX_FREE_QUEUE_SIZE of few hundred MB or with TCMALLOC_RECLAIM_MEMORY=false, it might help find leaks m
Exiting with error code (instead of crashing) because of whole-program memory leaks

这里虽然检测出了内存泄露,但是并没有打印出调用栈的符号信息,根据提示,可能是 PPROF_PATH 环境变量没有正确设置。按照如下方式设置环境变量:

1
2
3
4
# echo $PPROF_PATH
# which pprof
/usr/local/bin/pprof
# export PPROF_PATH=/usr/local/bin/pprof

再次运行,可以看到已经打印出了内存泄露的栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@36eab106d3bf gperftools-test]# env HEAPCHECK=normal ./memory_leak
WARNING: Perftools heap leak checker is active -- Performance may suffer
memory leak test
Have memory regions w/o callers: might report false leaks
Leak check _main_ detected leaks of 4 bytes in 1 objects
The 1 largest leaks:
Using local file ./memory_leak.
Leak of 4 bytes in 1 objects allocated from:
@ 4008ff func
@ 400935 main
@ 7f39d94db555 __libc_start_main
@ 400829 _start


If the preceding stack traces are not enough to find the leaks, try running THIS shell command:

pprof ./memory_leak "/tmp/memory_leak.16586._main_-end.heap" --inuse_objects --lines --heapcheck --edgefraction=1e-10 --nodefraction=1e-10 --gv

If you are still puzzled about why the leaks are there, try rerunning this program with HEAP_CHECK_TEST_POINTER_ALIGNMENT=1 and/or with HEAP_CHECK_MAX_POINTER_OFFSET=-1
If the leak report occurs in a small fraction of runs, try running with TCMALLOC_MAX_FREE_QUEUE_SIZE of few hundred MB or with TCMALLOC_RECLAIM_MEMORY=false, it might help find leaks m
Exiting with error code (instead of crashing) because of whole-program memory leaks

Reference