void foo(int y) {
cout << y << endl;
int main() {
int x;
foo(x);
return0;
在 main
函数中定义了一个没有被初始化的变量 x
,接下来传入 foo
函数,该函数的功能是打印传入的参数。由于变量 y
的值依赖于 x
,所以 y
的值是未定义的,此时打印变量 y
相当于间接使用了未初始化的变量,Memcheck 会报告这类错误。
原则 3,开启 -show-reachable=yes 命令行选项
强烈建议在运行 Memcheck 时增加 -show-reachable=yes
命令行选项,它可以帮我们检查全局指针、static 静态指针相关的内存泄漏问题。
强烈建议在进程结束时,正确而优雅的释放所有资源,包括关闭定时器和套接字、释放全局或者静态对象、回收线程资源等。培养严谨的编程风格。
为何一定要开启 reachable 命令行选项呢?别急,在原因揭晓之前,我们先来了解一下内存泄漏的定义以及 Memcheck 工具报告的四种内存泄漏形式。
究竟如何定义内存泄漏?
作者认为内存泄漏有如下两种场景:
内存已经分配,但是在进程结束之前没有被优雅的释放。
也就是说,在进程结束之前的那一刻,进程依然拥有指向该内存块的指针,指针并未丢失,仍然可以获取并访问(still reachable)。
具有进程级别的生命周期的静态指针或者全局指针指向的内存块没有在进程结束前被释放是造成这种场景下的内存泄漏的主要原因。
内存已经分配,但是在进程运行过程中不能被正常释放。
此时,进程不再拥有指向该内存块的指针,指针丢失。这种场景是为 c/c++ 开发者所熟知的真正意义上的 “内存泄漏”。造成这种场景下的内存泄漏的原因主要有:
开发者在编码过程中忘记了释放内存。
内存释放操作在某些异常处理逻辑之后,而这些异常处理逻辑在 return 之前并未做好内存释放的工作。
一些需要实时缓存的数据虽然在连接建立时能被正常释放,但是在连接断开时却并未做好资源清理工作,比如流媒体服务中的重传缓存、gop 缓存。
Memcheck 输出的四种内存泄漏形式
内存检查报告按照丢失字节数从小到大排序展示。下面来认识下 Memcheck 工具输出的检查报告中的四种内存泄漏形式:
definitely lost,指针确认丢失。
当进程在运行或者进程结束时,如果一块动态分配的内存没有被释放,并且程序中已经找不到能够正常访问这块内存的指针,则会报这个错误。也就是说指针已丢失,但是内存未释放,这是真正的需要被关注的内存泄漏,需要尽快修复。
indirectly lost,指针间接丢失。
当使用了含有指针成员的类或结构时可能会报这个错误。这类错误无需直接修复,他们总是与 definitely lost
一起出现,只要修复 definitely lost
即可。
possibly lost,指针可能丢失。
当进程结束时,如果一块动态分配的内存没有被释放,且通过程序内的指针均无法访问这块内存的起始地址,但是可以访问这块内存的部分数据时,那么指向该内存块的指针可能丢失。也就是说原本指向内存起始地址的指针被重新指向了这块内存的中间的某个地址(即非起始地址)则会报这个错误。
大多数情况下应视为与 definitely lost
一样需要尽快修复,除非这是你有意而为之,并且你可以让已经指向内存非起始地址的指针经过某些运算重新指向这块内存的起始地址并释放它。
still reachable,仍然可以获取指针并访问内存。
指针未丢失,内存未释放。如果程序是正常结束的,那么这类报错一般不会造成程序 crash,一般可以忽略掉。
这类指针基本上是静态指针或者全局指针,所以这些 still reachable
的内存块通常是只分配一次,并且具有进程级别的生命周期,正如 valgrind 官方手册描述的那样:
these blocks are usually one-time allocations, references to which are kept throughout the duration of the process's lifetime.
综上,对于这四种不同的内存泄漏形式,我们应该按照 definitely lost
、possibly lost
、still reachable
的顺序依次解决。
still reachable 是内存泄漏吗?
其实,这种场景下的泄漏在严格意义上来讲也许并不能称之为内存泄漏,因为在进程运行过程中并没有泄漏问题。
虽然内存在进程结束之前确实未被释放,但是指向这块内存的指针是 reachable
的,操作系统会获取这些指针并帮助我们释放内存。
但是,请注意,still reachable
可能会掩盖真正的内存泄漏 definitely lost
,这就是作者为何强烈建议开启 reachable 命令行选项的原因。
作者曾经遇到过一个非常隐秘的内存泄漏问题:某次查看线上服务物理内存占用达到了 2G,开始以为是底层 jemalloc 未将内存归还操作系统导致,再加之 Memcheck 并未报出 definitely lost
错误,所以并没有认为是内存泄漏。过了一周,再次查看发现内存占用已经超过了 10G,这次毋庸置疑,绝对是内存泄漏了,但是 Memcheck 仍然检测不出哪里泄漏。最终不得已开启了 reachable 选项,让 Memcheck 报告出所有的 still reachable
信息,逐一排查这些可疑信息,终于定位了内存泄漏的点:原来是拉流缓存的数据包未在用户停止拉流后释放。后来,再次回顾这次解决内存泄漏的过程,发现逐一排查 still reachable
信息定位问题实在是效率低下,况且这次内存泄漏为何没有被报告出 definitely lost
错误?这是个问题。最终,将数据缓存结构的上层全局指针在进程退出时主动释放,结果这一次的内存检查报告不仅精确的定位到了内存泄露的地方,而且也没有了 still reachable
的错误。
所以,作者强烈建议养成在进程结束之前优雅的释放掉静态 / 全局指针、做好资源的清理工作的良好编程习惯,并在使用 Memcheck 时开启 reachable 参数,竭尽所能的消灭 still reachable
报错,这样不仅能暴露 definitely lost
错误,检查报告看起来也会清爽很多。
原则 4,周密思考!保证 Memcheck 测试到程序的每一个逻辑分支
在运行 Memcheck 之前,我们要周密的思考,列举出所有重要的测试场景,确保最大化的发挥 Memcheck 的作用。比如下面这几种测试场景就很重要:
弱网场景下是否进行了测试?
实验室环境总是比较理想的,也许 Memcheck 测试不出程序应对弱网环境的逻辑漏洞,所以,在丢包、延迟、乱序的弱网环境下使用 Memcheck 才能真正的暴露问题。
进程结束前的资源清理和释放逻辑是否进行了测试?
也就是说,你的程序是否具有捕捉并处理信号的能力?比如,捕捉并处理了 SIGINT 或者 SIGTERM 信号,那么当执行 ctrl + c
后,Memcheck 就可以在进程结束前检查信号处理函数的处理逻辑。
如果程序在退出逻辑中未对一些资源(内存,套接字,定时器,io 事件等)做释放,那么 Memcheck 会检查到这些错误,也许是 still reachable
错误,上文已经提到,这个错误建议解决。
进程运行时的一些异常处理逻辑是否测试到位?
比如对于流媒体服务来讲,停止推拉流、推拉流失败、回源失败等相关的逻辑是否被测试到。
Memcheck 四种指针丢失情形的代码演示
definitely lost 与 still reachable 代码演示
首先,我们先演示绝对丢失和 still reachable
这两种情况。
void test01() {
char* p = newchar[1024];
void test02() {
staticchar* p = newchar[1024];
int main() {
test01();
test02();
return0;
在 test01 中,new
出来的数组赋值给局部指针变量 p
,test01 测试结束后,局部变量 p
丢失,内存未被释放,造成内存泄漏,Memcheck 会报告 definitely lost
错误。
在 test02 中,new
出来的数组赋值给具有进程级生命周期的静态指针变量 p
,test02 测试结束后直到 main
函数返回前,静态指针 p
依然可以获取到,但是内存并未在进程结束前释放,Memcheck 会报告 still reachable
错误。
indirectly lost 代码演示
接下来演示间接丢失的情况。
class Object {
public:
Object() { _p = newchar[1024]; }
~Object() { if(_p) delete _p; }
private:
char* _p = nullptr;
void test03() {
Object* obj = new Object();
int main() {
test03();
return0;
在 test03 中,我们 new
了一个 Object
类型的局部对象指针 obj
,它的成员 _p
指向动态分配的数组,test03 测试结束后,局部变量 obj
丢失,内存未被释放且其内部成员 _p
指针也间接丢失,没有被释放。Memcheck 会报告 definitely lost
和 indirectly lost
错误。
possibly lost 代码演示
接下来演示可能丢失的情况。
void test04() {
char* data = newchar[1024];
staticchar* p = data + 1;
int main() {
test04();
return0;
在 test04 中,我们 new
一个数组并返回给局部变量 data
,随后声明静态指针 p
并指向数组第二个元素的地址,test04 测试结束后直到 main
函数返回前,静态指针 p
仍然可获得,但是 p
已经不再指向数组的起始地址。Memcheck 认为指向这块内存的指针可能已经丢失,会报告 possibly lost
错误。
接下来,我们在 test04 函数中增加一行代码 p = data;
。
void test04() {
char* data = newchar[1024];
staticchar* p = data + 1;
p = data;
此时,静态指针 p
重新指向了数组的起始地址,所以 Memcheck 不会再报告 possibly lost
错误。但是 Memcheck 会报告 still reachable
错误,这是因为静态指针指向的数组空间没有被释放,在测试进程结束前仍然可以获取到导致,只要再加一行 delete [] data
或者 delete [] p
即可解决。
最后,我们在 test04 函数中再增加一行代码 p = nullptr;
。
void test04() {
char* data = newchar[1024];
staticchar* p = data + 1;
p = data;
p = nullptr;
现在,Memcheck 又会输出什么呢?答案是输出 definitely lost
错误。因为 p
为空指针,不指向任何已分配的内存块,且没有指向数组的非起始地址,所以不会有 still reachable
和 possibly lost
这两种错误。
此时,只有局部指针 data
指向数组首地址,但是在 test04 函数测试结束之前我们并没有释放这块内存,所以 test04 测试结束后局部指针 data
确认丢失,程序出现内存泄漏。
still reachable 掩盖 definitely lost 代码演示
最后来演示未释放全局或者静态指针导致 still reachable
掩盖了 definitely lost
报错的情况。
下面的代码就是模拟的上文提到那次隐秘的线上服务内存泄漏问题。简单描述一下代码逻辑:首先有一个 RtcStreamMgr
类型的全局指针,该类的内部成员是一个流名到数据包缓存队列的映射。接下来构造一个流名为 666,数据包缓存队列大小为 1 的键值对并插入到 map。最后来模拟删除 map 中流名为 666 的元素时忘记了 delete 其对应数据包缓存队列的场景。
class RtcPacket {
public:
RtcPacket(int seq, int len)
: _seq(seq), _len(len) {}
~RtcPacket() {}
private:
int _seq;
int _len;
class RtcStreamMgr {
public:
std::map<std::string, std::list<
std::shared_ptr<RtcPacket>>*>
rtc_packet_map;
auto g_stream_mgr = new RtcStreamMgr();
void test05() {
// 构造缓存数据包的map
std::shared_ptr<RtcPacket>
packet(new RtcPacket(1, 1024));
autolist = newstd::list<
std::shared_ptr<RtcPacket>>();
list->push_back(packet);
g_stream_mgr->rtc_packet_map["666"] = list;
// 删除map元素,但未删除该元素对应的动态内存
auto it = g_stream_mgr->rtc_packet_map.find("666");
g_stream_mgr->rtc_packet_map.erase(it);
int main() {
test05();
return0
首先,删除 map 元素时未释放其对应的动态内存,显然,这会造成内存泄漏。其次,全局对象 g_stream_mgr
也是动态分配的内存,但是由于其生命周期是进程级,所以很多开发者不会在进程退出前去主动释放它,即使在原则上我们确实该释放它。然而,问题出现了:
当在进程退出前不主动释放全局对象 g_stream_mgr
时,Memcheck 输出的都是 still reachable 错误。
这使得大多数开发者认为自己的程序并没有真正的内存泄漏问题,于是不会仔细阅读大篇幅的 reacable 报错,也就无法解决内存泄漏问题。
当在进程退出前主动释放全局对象 g_stream_mgr
时,Memcheck 不再输出 still reachable
错误,而是精确的输出了 definitely lost
错误。
这使得开发者一眼便定位到了内存泄漏问题并轻松的解决它。
所以这就是上文提到的问题:在某些场景下,still reachable
报错会掩盖掉 definitely lost
报错,从而加大内存泄漏问题的排查难度。
不过这个掩盖的问题作者只在工作的开发机(CentOS,gcc 4.8.4,glibc 2.12,valgrind 3.11.0)上复现过,当为写这篇文章准备再次复现时(因为某些原因,之前复现过的开发机被回收了,只能在其他机器上复现)却无论如何也无法复现,回天乏术。
不过这也是个好消息,这意味着无论是否主动释放全局或者静态指针,都能精准定位到真正的内存泄漏问题。
最后,完整的内存泄漏演示代码 [1] 已经提交到了我的 github,你可以下载并亲自动手去验证。
Valgrind 的编译与使用
最后,说一下如何使用 valgrind,非常简单。首先通过 wget 命令下载 valgrind。
wget http://valgrind.org/downloads/valgrind-3.16.1.tar.bz2
接着执行 ./configure && make && make install,完成编译与安装。最后运行 valgrind,只需要执行下面的命令即可。
valgrind --tool=memcheck --leak-check=full --show-reachable=yes --log-file=path_of_log path_of_bin
也可以不指定 --took=memcheck
,因为 Memcheck 是默认工具。
在运行 valgrind 时可能并不会一帆风顺,可能会出现如下报错:
valgrind: the 'impossible' happened: LibVEX called failure_exit().
遇到这种情况时,在运行时增加命令行选项 --vex-guest-max-insns=2
即可解决问题。
也有可能会出现如下报错:
valgrind: failed to start tool 'memcheck' for platform 'amd64-linux': No such file or directory
遇到这种情况时,我们需要执行 autogen.sh 脚本,之后再重新编译并安装 valgrind。
另外,还有几点需要说明:
在使用前需要保证你的可执行文件已经在编译时增加了产生调试信息的命令行参数 -g,否则检查报告不会输出问题代码的具体行数。
根据 Valgrind 的官方文档,它会导致可执行文件的速度减慢 20 至 30 倍。所以一般来讲,Valgrind 是无法应用到压力测试的场景之中的。
结束 Memcheck 检查的做法一般是发送 SIGINT 信号,即 ctrl + c。不要发送 SIGKILL 信号结束进程,否则无法生成检查报告。
关于 Memcheck 输出信息与相关命令行的更详尽且权威的介绍以及 Memcheck 的检测原理,可以阅读 valgrind-memcheck 官方手册 [2] 。
最后,希望大家编写的程序能够输出和下图一样的 Memcheck 检查报告:no leaks,no errors。
完美的 memcheck 检查报告
至此,本文结束,感谢阅读。
[1]valgrind_memcheck.cpp: https://github.com/yujitai/valgrind_test
[2]Memcheck: a memory error detector: https://www.valgrind.org/docs/manual/mc-manual.html
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。