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

内存泄漏

一、内存泄漏的危害:

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

如果有个进程可用的内存不够,就会触发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软件,集成了一些静态代码分析的工具

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

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

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

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

(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的断点命中次数,如果次数一样,那没问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值