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 checkerheap 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