内存泄漏
一、内存泄漏的危害:
内存泄漏会导致当前应用程序消耗更多的内存,使得其他应用程序可用的内存更少了。
如果有个进程可用的内存不够,就会触发Linux操作系统的直接/后台内存回收(即将一些内存页的数据写到磁盘里,那么该页也就可用了,脏页回写)。虽然后台回收是异步的不阻塞当前进程,但是内存还是不够会触发直接内存回收,最后内存泄漏积累到一定程度,会直接触发OOM,该机制会杀掉那些实时占用内存大的进程。
而且,即使没有OOM,无论是直接回收还是后台回收,都需要磁盘I/O而且需要多执行额外的回收线程,使系统性能下降。
- 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
- 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行,这个过程比较慢,导致CPU占用率飙升。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM (Out of Memory)机制。
还有资源泄漏:
比如没有关闭文件,程序提前return或报错或忘记关闭,则可能导致想写入文件的数据没有真正落盘,从而丢失数据。
二、内存泄漏举例:
1,在free()前就返回了,或者是报错并退出程序。要在程序的所有路径上(if()的各个条件)都执行资源释放操作。
2,在析构函数中未执行内存释放操作。在构造函数中申请了堆内存或者打开了文件,在析构函数中忘了释放资源。
3,基类的析构函数未声明为虚函数。
析构函数如果不声明为虚函数,可能会导致多态对象在删除时无法正确调用派生类的析构函数(如果子类构造函数里malloc()了内存,然后在析构函数里free()),从而导致内存泄漏。
4,shared_ptr循环引用导致内存泄漏,用weak_ptr解决。如下示例:
class Contro {
private:
double* p;
public:
Contro() {
p = new double[10];
}
~Contro() {
delete[] p;
std::cout << "in ~Contro" << std::endl;
}
// 类内类
class SubContro {
public:
SubContro() {
p = new double[10];
}
~SubContro() {
delete[] p;
std::cout << "in ~SubContro" << std::endl;
}
std::shared_ptr<Contro> controller_;
};
std::shared_ptr<SubContro> sub_controller_;
};
int main() {
auto contro = std::make_shared<Contro>();
auto sub_contro = std::make_shared<Contro::SubContro>();
contro->sub_controller_ = sub_contro;
sub_contro->controller_ = contro;
// 打印引用计数
std::cout << "contro use_count: " << contro.use_count() << std::endl;
std::cout << "sub_contro use_count: " << sub_contro.use_count() << std::endl;
return 0;
}
发生循环引用,两个的引用计数输出都是2,所以main函数结束的时候,引用计数没有减为0,就不会调用二者的析构函数,导致资源泄漏。
将SubContro类里的shared_ptr改成weak_ptr即可,后者不会增加引用计数,因此两个智能指针的引用计数都是1,然后main结束的时候,引用计数减少为0,然后执行析构函数,此时不会发生内存泄漏,输出如下:
contro use_count: 1
sub_contro use_count: 2
in ~Contro
in ~SubContro
5,生产者—消费者模型中,消费不及时导致内存泄漏。
比如一个队列是由两个线程共享,其中一个写入数据,一个取出数据,但是后者取出后,执行一些阻塞式的函数,比如通过网络发送数据write_data(),该函数只有在发送完或者报错了才返回,而遇上网络情况不好时则会阻塞返回,导致消费者对队列中数据消费不及时,使得队列中数据越来越多,造成内存占用显著升高。
三、避免内存泄漏的手段:
1. 静态代码检查工具
(1)对于大型项目,可以使用静态代码分析工具
像开源的有codechecker软件,集成了一些静态代码分析的工具
静态代码检查工具会从词法、语法、语义等多维度去对工程代码扫描分析,发现可能存在的问题,比如变量未定义、类型不匹配、变量作用域问题、数组下标越界、内存泄露等问题。
既然是静态,那么就不是运行时。但是是编译前还是编译后还是编译中?
其实都有,像商业软件“啄木鸟”是给源文件就行,然后它会在编译的过程中去检测语法,词法,以及最后生成的二进制。
(2)编译成专门的内存泄漏检查版本。
可以把整个项目编译成检查内存泄漏版本的可执行文件,然后运行相关工具,并且让运行结果专门记录内存泄漏,将泄漏结果放在对应输出文件上。
比如opengauss就有,参考链接:http://t.csdn.cn/DqusO
编译openGauss时,编译一个memcheck版本的,然后通过跑fastcheck_single来发现代码中的内存问题。 编译方式和编译普通的openGauss基本一致,只是在configure时,添加一个 --enable-memory-check
参数,编译出来的就是memcheck版本的openGauss。
但是编译前,要设置一些环境变量,ulimit -v unlimited
ulimit命令:用于控制shell程序的资源, -v <虚拟内存大小>
指定可使用的虚拟内存上限,单位为KB。
因为可能有内存泄漏,所以就设置虚拟内存大小为不受限制。
其实这里memcheck版本是用的ASAN工具
2. valgrind工具
可以安装valgrind工具,指定工具--tool=memcheck
,也可以指定输出日志,否则输出在终端
--log-file=leak1.log
对可执行文件a.out,执行如下命令:
valgrind --log-file=valgrind.log --tool=memcheck --leak-check=full --show-leak-kinds=all ./a.out
如下可以看到总的malloc和free的次数,以及被申请的字节数,在每一个内存泄漏的地方,也会显示函数调用堆栈,便于追踪:
(注:图片相关函数做了打码处理)
这个工具的用法还挺多,可以参考https://zhuanlan.zhihu.com/p/92074597
3. GDB调试
比如我们怀疑FUNC()
函数有内存泄漏。
1,比如给某个函数FUNC()
打断点,进入后这个函数里面也调用了很多其他函数func1,func2…,怀疑这些调用里面,或者外面有内存泄漏。我们可以给malloc()和free()打断点(或者是自己封装的函数),当malloc()命中后,bt查看栈帧,就知道哪个函数调用了malloc,申请了堆内存,比如func1,这样可以重点关注该函数。
2,然后看后面free()
断点有没有命中,命中的时候查看栈帧,如果不是这个函数func1调用的free()
,那说明这个函数没有执行free。
3,此外,可以追踪指针p的值(watch p
),看看它有没有变为0x0,被释放且被赋值为0x0,才不会成为悬空指针。
4,在函数FUNC()
的末尾,还可以看看malloc和free的断点命中次数,如果次数一样,那没问题。