首页 >> 大全

C/C++内存泄漏原因分析与应对方法

2023-09-19 大全 31 作者:考证青年

内存泄漏 一、内存泄漏的危害:

内存泄漏会导致当前应用程序消耗更多的内存,使得其他应用程序可用的内存更少了。

如果有个进程可用的内存不够,就会触发Linux操作系统的直接/后台内存回收(即将一些内存页的数据写到磁盘里,那么该页也就可用了,脏页回写)。虽然后台回收是异步的不阻塞当前进程,但是内存还是不够会触发直接内存回收,最后内存泄漏积累到一定程度,会直接触发OOM,该机制会杀掉那些实时占用内存大的进程。

而且,即使没有OOM,无论是直接回收还是后台回收,都需要磁盘I/O而且需要多执行额外的回收线程,使系统性能下降。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM (Out of )机制。

还有资源泄漏:

比如没有关闭文件,程序提前或报错或忘记关闭,则可能导致想写入文件的数据没有真正落盘,从而丢失数据。

二、内存泄漏举例:

1,在free()前就返回了,或者是报错并退出程序。要在程序的所有路径上(if()的各个条件)都执行资源释放操作。

2,在析构函数中未执行内存释放操作。在构造函数中申请了堆内存或者打开了文件,在析构函数中忘了释放资源。

3,基类的析构函数未声明为虚函数。

析构函数如果不声明为虚函数,可能会导致多态对象在删除时无法正确调用派生类的析构函数(如果子类构造函数里()了内存,然后在析构函数里free()),从而导致内存泄漏。

4,循环引用导致内存泄漏,用解决。如下示例:

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,就不会调用二者的析构函数,导致资源泄漏。

将类里的改成即可,后者不会增加引用计数,因此两个智能指针的引用计数都是1,然后main结束的时候,引用计数减少为0,然后执行析构函数,此时不会发生内存泄漏,输出如下:

contro use_count: 1
sub_contro use_count: 2
in ~Contro
in ~SubContro

三、避免内存泄漏的手段: 1. 静态代码检查工具 (1)对于大型项目,可以使用静态代码分析工具

像开源的有软件,集成了一些静态代码分析的工具

静态代码检查工具会从词法、语法、语义等多维度去对工程代码扫描分析,发现可能存在的问题,比如变量未定义、类型不匹配、变量作用域问题、数组下标越界、内存泄露等问题。

既然是静态,那么就不是运行时。但是是编译前还是编译后还是编译中?

其实都有,像商业软件“啄木鸟”是给源文件就行,然后它会在编译的过程中去检测语法,词法,以及最后生成的二进制。

代码静态分析(SAST):可以简单的理解为在不执行程序的情况下,对源代码, 中间代码或者二进制代码进行分析的技术

(2)编译成专门的内存泄漏检查版本。

可以把整个项目编译成检查内存泄漏版本的可执行文件,然后运行相关工具,并且让运行结果专门记录内存泄漏,将泄漏结果放在对应输出文件上。

比如就有,参考链接:

编译时,编译一个版的,然后通过跑来发现代码中的内存问题。 编译方式和编译普通的基本一致,只是在时,添加一个 ----check 参数,编译出来的就是版本的。

但是编译前,要设置一些环境变量, -v

命令:用于控制shell程序的资源, -v 指定可使用的虚拟内存上限,单位为KB。

因为可能有内存泄漏,所以就设置虚拟内存大小为不受限制。

2. 工具

可以安装工具,指定工具--tool=,也可以指定输出日志,否则输出在终端

--log-file=leak1.log

对可执行文件a.out,执行如下命令:

valgrind --log-file=valgrind.log --tool=memcheck --leak-check=full --show-leak-kinds=all ./a.out

如下可以看到总的和free的次数,以及被申请的字节数,在每一个内存泄漏的地方,也会显示函数调用堆栈,便于追踪:

(注:图片相关函数做了打码处理)

这个工具的用法还挺多,可以参考

3. GDB调试

比如我们怀疑FUNC()函数有内存泄漏。

1,比如给某个函数FUNC()打断点,进入后这个函数里面也调用了很多其他函数func1,func2…,怀疑这些调用里面,或者外面有内存泄漏。我们可以给()和free()打断点(或者是自己封装的函数),当()命中后,bt查看栈帧,就知道哪个函数调用了,申请了堆内存,比如func1,这样可以重点关注该函数。

2,然后看后面free()断点有没有命中,命中的时候查看栈帧,如果不是这个函数func1调用的free(),那说明这个函数没有执行free。

3,此外,可以追踪指针p的值(watch p),看看它有没有变为0x0,被释放且被赋值为0x0,才不会成为悬空指针。

4,在函数FUNC()的末尾,还可以看看和free的断点命中次数,如果次数一样,那没问题。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了