C++ 如何避免内存泄露?
旺道一键推广 -> 最新发布

(给程序员的那些事加星标

来源:知乎-张凯(Kyle Zhang)

【CPP开发者导读】:内存泄漏是C/C++的一个老生常谈的问题,无论是新手还是有经验的开发者都会在这个问题上栽跟头。


本文向读者介绍了如何避免内存泄漏的方法和原则,在细节和大体方向上均给出一些可行性方案。
读者可以尝试文中提出的方法,改进自己的代码,大大减少内存泄漏的可能性。



以下是正文



前言


近年来,讨论 C++ 的人越来越少了,一方面是由于像 Python,Go 等优秀的语言的流行,另一方面,大家也越来越明白一个道理,并不是所有的场景都必须使用 C++ 进行开发。Python 可以应付大部分对性能要求不高的场景,Go 可以应付大部分对并发要求较高的场景,而由于 C++ 的复杂性,只有在对性能极其苛刻的场景下,才会考虑使用。


那么到底多苛刻算是苛刻呢?Go 自带内存管理,也就是 GC 功能,经过多年的优化,在 Go 中每次 GC 可能会引入 500us 的 STW 延迟。


也就是说,如果你的应用场景可以容忍不定期的 500us 的延迟,那么用 Go 都是没有问题的。如果你无法容忍 500us 的延迟,那么带 GC 功能的语言就基本无法使用了,只能选择自己管理内存的语言,例如 C++。那么由手动管理内存而带来的编程复杂度也就随之而来了。

作为 C++ 程序员,内存泄露始终是悬在头上的一颗炸弹。在过去几年的 C++ 开发过程中,由于我们采用了一些技术,我们的程序发生内存泄露的情况屈指可数。今天就在这里向大家做一个简单的介绍。


内存是如何泄露的


在 C++ 程序中,主要涉及到的内存就是『栈』和『堆』(其他部分不在本文中介绍了)。



通常来说,一个线程的栈内存是有限的,通常来说是 8M 左右(取决于运行的环境)。栈上的内存通常是由编译器来自动管理的。当在栈上分配一个新的变量时,或进入一个函数时,栈的指针会下移,相当于在栈上分配了一块内存。我们把一个变量分配在栈上,也就是利用了栈上的内存空间。当这个变量的生命周期结束时,栈的指针会上移,相同于回收了内存。

由于栈上的内存的分配和回收都是由编译器控制的,所以在栈上是不会发生内存泄露的,只会发生栈溢出(Stack Overflow),也就是分配的空间超过了规定的栈大小。

而堆上的内存是由程序直接控制的,程序可以通过 malloc/free 或 new/delete 来分配和回收内存,如果程序中通过 malloc/new 分配了一块内存,但忘记使用 free/delete 来回收内存,就发生了内存泄露。


经验 #1:尽量避免在堆上分配内存


既然只有堆上会发生内存泄露,那第一原则肯定是避免在堆上面进行内存分配,尽可能的使用栈上的内存,由编译器进行分配和回收,这样当然就不会有内存泄露了。

然而,只在栈上分配内存,在有 IO 的情况下是存在一定局限性的。
举个例子,为了完成一个请求,我们通常会为这个请求构造一个 Context 对象,用于描述和这个请求有关的一些上下文。例如下面一段代码:

void Foo(Reuqest* req) {    RequestContext ctx(req);    HandleRequest(&ctx);}


如果 HandleRequest 是一个同步函数,当这个函数返回时,请求就可以被处理完成,那么显然 ctx 是可以被分配在栈上的。

但如果 HandleRequest 是一个异步函数,例如:

void HandleRequest(RequestContext* ctx, Callback cb);


那么显然,ctx 是不能被分配在栈上的,因为如果 ctx 被分配在栈上,那么当 Foo 函数推出后,ctx 对象的生命周期也就结束了。而 FooCB 中显然会使用到 ctx 对象。


void HandleRequest(RequestContext* ctx, Callback cb);
void Foo(Reuqest* req) { auto ctx = new RequestContext(req); HandleRequest(ctx, FooCB);}
void FooCB(RequestContext* ctx) { FinishRequest(ctx); delete ctx;}

在这种情况下,如果忘记在 FooCB 中调用 delete ctx,则就会触发内存泄露。尽管我们可以借助一些静态检查工具对代码进行检查,但往往异步程序的逻辑是极其复杂的,一个请求的生命周期中,也需要进行大量的内存分配操作,静态检查工具往往无法发现所有的内存泄露情况。

那么怎么才能避免这种情况的产生呢?引入智能指针显然是一种可行的方法,但引入 shared_ptr 往往引入了额外的性能开销,并不十分理想。
在 SmartX,我们通常采用两种方法来应对这种情况。


经验 #2:使用 Arena


Arena 是一种统一化管理内存生命周期的方法。所有需要在堆上分配的内存,不通过 malloc/new,而是通过 Arena 的 CreateObject 接口。同时,不需要手动的执行 free/delete,而是在 Arena 被销毁的时候,统一释放所有通过 Arena 对象申请的内存。所以,只需要确保 Arena 对象一定被销毁就可以了,而不用再关心其他对象是否有漏掉的 free/delete。这样显然降低了内存管理的复杂度。

此外,我们还可以将 Arena 的生命周期与 Request 的生命周期绑定,一个 Request 生命周期内的所有内存分配都通过 Arena 完成。这样的好处是,我们可以在构造 Arena 的时候,大概预估出处理完成这个 Request 会消耗多少内存,并提前将会使用到的内存一次性的申请完成,从而减少了在处理一个请求的过程中,分配和回收内存的次数,从而优化了性能。

我们最早看到 Arena 的思想,是在 LevelDB 的代码中。这段代码相当简单,建议大家直接阅读。


经验 #3:使用 Coroutine


Coroutine 相信大家并不陌生,那 Coroutine 的本质是什么?我认为 Coroutine 的本质,是使得一个线程中可以存在多个上下文,并可以由用户控制在多个上下文之间进行切换。而在上下文中,一个重要的组成部分,就是栈指针。使用 Coroutine,意味着我们在一个线程中,可以创造(或模拟)多个栈。

有了多个栈,意味着当我们要做一个异步处理时,不需要释放当前栈上的内存,而只需要切换到另一个栈上,就可以继续做其他的事情了,当异步处理完成时,可以再切换回到这个栈上,将这个请求处理完成。
还是以刚才的代码为示例:
void Foo(Reuqest* req) {    RequestContext ctx(req);    HandleRequest(&ctx);}
void HandleRequest(RequestCtx* ctx) { SubmitAsync(ctx); Coroutine::Self()->Yield(); CompleteRequest(ctx);}

这里的精髓在于,尽管 Coroutine::Self()->Yield() 被调用时,程序可以跳出 HandleRequest 函数去执行其他代码逻辑,但当前的栈却被保存了下来,所以 ctx 对象是安全的,并没有被释放。

这样一来,我们就可以完全抛弃在堆上申请内存,只是用栈上的内存,就可以完成请求的处理,完全不用考虑内存泄露的问题。然而这种假设过于理想,由于在栈上申请内存存在一定的限制,例如栈大小的限制,以及需要在编译是知道分配内存的大小,所以在实际场景中,我们通常会结合使用 Arena 和 Coroutine 两种技术一起使用。

有人可能会提到,想要多个栈用多个线程不就可以了?然而用多线程实现多个栈的问题在于,线程的创建和销毁的开销极大,且线程间切块,也就是在栈之间进行切换的代销需要经过操作系统,这个开销也是极大的。所以想用线程模拟多个栈的想法在实际场景中是走不通的。

关于 Coroutine 有很多开源的实现方式,大家可以在 github 上找到很多,C++20 标准也会包含 Coroutine 的支持。在 SmartX 内部,我们很早就实现了 Coroutine,并对所有异步 IO 操作进行了封装,示例可参考我们之前的一篇文章 smartx:基于 Coroutine 的异步 RPC 框架示例(C++)

这里需要强调一下,Coroutine 确实会带来一定的性能开销,通常 Coroutine 切换的开销在 20ns 以内,然而我们依然在对性能要求很苛刻的场景使用 Coroutine,一方面是因为 20ns 的性能开销是相对很小的,另一方面是因为 Coroutine 极大的降低了异步编程的复杂度,降低了内存泄露的可能性,使得编写异步程序像编写同步程序一样简单,降低了程序员心智的开销。

boost 实现的 coroutine 的性能测试结果


经验 #4:善用 RAII


尽管在有些场景使用了 Coroutine,但还是可能会有在堆上申请内存的需要,而此时有可能 Arena 也并不适用。在这种情况下,善用 RAII(Resource Acquisition Is Initialization)思想会帮助我们解决很多问题。

简单来说,RAII 可以帮助我们将管理堆上的内存,简化为管理栈上的内存,从而达到利用编译器自动解决内存回收问题的效果。此外,RAII 可以简化的还不仅仅是内存管理,还可以简化对资源的管理,例如 fd,锁,引用计数等等。

当我们需要在堆上分配内存时,我们可以同时在栈上面分配一个对象,让栈上面的对象对堆上面的对象进行封装,用时通过在栈对象的析构函数中释放堆内存的方式,将栈对象的生命周期和堆内存进行绑定。

unique_ptr 就是一种很典型的例子。然而 unique_ptr 管理的对象类型只能是指针,对于其他的资源,例如 fd,我们可以通过将 fd 封装成另外一个 FileHandle 对象的方式管理,也可以采用一些更通用的方式。例如,在我们内部的 C++ 基础库中实现了 Defer 类,想法类似于 Go 中 defer。
void Foo() {    int fd = open();    Defer d = [=]() { close(fd); }    // do something with fd}

经验 #5:便于 Debug


在特定的情况下,我们难免还是要手动管理堆上的内存。然而当我们面临一个正在发生内存泄露线上程序时,我们应该怎么处理呢?

当然不是简单的『重启大法好』,毕竟重启后还是可能会产生泄露,而且最宝贵的现场也被破坏了。最佳的方式,还是利用现场进行 Debug,这就要求程序具有便于 Debug 的能力。

这里不得不提到一个经典而强大的工具 gperftools。gperftools 是 google 开源的一个工具集,包含了 tcmalloc,heap profiler,heap checker,cpu profiler 等等。gperftools 的作者之一,就是大名鼎鼎的 Sanjay Ghemawat,没错,就是与 Jeff Dean 齐名,并和他一起写 MapReduce 的那个 Sanjay。


gperftools 的一些经典用法,我们就不在这里进行介绍了,大家可以自行查看文档。而使用 gperftools 可以在不重启程序的情况下,进行内存泄露检查,这个恐怕是很少有人了解。

实际上我们 Release 版本的 C++ 程序可执行文件在编译时全部都链接了 gperftools。在 gperftools 的 heap profiler 中,提供了 HeapProfilerStart 和 HeapProfilerStop 的接口,使得我们可以在运行时启动和停止 heap profiler。同时,我们每个程序都暴露了 RPC 接口,用于接收控制命令和调试命令。在调试命令中,我们就增加了调用 HeapProfilerStart 和 HeapProfilerStop 的命令。由于链接了 tcmalloc,所以 tcmalloc 可以获取所有内存分配和回收的信息。当 heap profiler 启动后,就会定期的将程序内存分配和回收的行为 dump 到一个临时文件中。

当程序运行一段时间后,你将得到一组 heap profile 文件
  profile.0001.heap  profile.0002.heap  ...  profile.0100.heap
每个 profile 文件中都包含了一段时间内,程序中内存分配和回收的记录。如果想要找到内存泄露的线索,可以通过使用
pprof --base=profile.0001.heap /usr/bin/xxx profile.0100.heap --text
来进行查看,也可以生成 pdf 文件,会更直观一些。


这样一来,我们就可以很方便的对线上程序的内存泄露进行 Debug 了。

写在最后


C++ 可谓是最复杂、最灵活的语言,也最容易给大家带来困扰。如果想要用好 C++,团队必须保持比较成熟的心态,团队成员必须愿意按照一定的规则来使用 C++,而不是任性的随意发挥。这样大家才能把更多精力放在业务本身,而不是编程语言的特性上。




- EOF -


推荐阅读  点击标题可跳转

1、详解C++字符编码的转换

2、C++异常机制概述

3、C++ string 类详解



关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

↓↓↓


点赞和在看就是最大的支持❤️

教育培训机构-如何策划节日招生?
做教育培训,最重要的事情莫过于招生了。面对不同的学员,招生方…于招生。目标不同,活动策划也会不一样。② 活动主题要鲜明不仅主题要鲜明,主题之间要有连续性。就像看连续剧一样,一集接一集,一环扣一环。策划好一系列活动,一个接一个来,从多个角度延续活动的宣传,突出主题,引起关注度。常用的招生方式有:体验课招…-------------想要获得更多运营技巧、doc文案,
教育培训:如何提高满班率和续班率
什么是满班率?就是实际人数除以计划招生人数,这个值就是满班率…员有流失是正常的,但要保证一个概率。在保障老学员续期的基础上,我们也要大胆创新,开设网络课程,拓宽招生区域。实地课程录制后,进一步发行到商弈云课上,通过商弈云课系统转成真实流量,网上有人点击观看,点赞,可以按点播量分佣金。助你在日常的营销推…-----------------------想要获得更多运
培训机构难盈利的问题出在哪里,如何实现盈利?
竞争白热化实地培训班、托管辅导班竞争白热化了,利润越来越稀薄…现没有多少盈利。还有一部分负责人在刚招生时,掌握了不少资金,开始盲目扩张,盲目做课程推广,以期待获得更多生源和收费,从而使培训班陷入了这种恶性循环的陷阱里。其实这些到手的钱,并不是你的利润,而是你未来要支出的钱。如遇到行业快速发展时期,或者…-----想要获得更多运营技巧、doc文件,请“关注”并“转
“差异化”经营是网络培训赚钱的关键所在
“差异化经营”这个词,相信大多数人都耳熟能详,但要做到的却是…。在现有的基础上更新升级或者资源整合就是对现有的产品或者技术,进行升级版本,或者通过一些资源整合,成一个项目。你的技能以前是用来生产或制造产品,现在是把技能升级成知识培训,转换云课堂。直白一点就是可以让老师自行录好课程并上传到学校的商弈云课…在着缺点和不足之处。在创新改革时,要从功能、服务、价格等方面
教育培训行业是如何运作的?
以前读书时,学校附近有个小摊卖鸡蛋灌饼。刚开始的时候只有一家….%,剩下的一对一和多人大班其实市场份额都很小。家长报培训班,是希望孩子能够学到知识。老师通过课上教学和课后辅导来满足孩子的需求,达到成绩提高的目标。我见过上百人的大培训班,为什么一个班这么多人?因为是名师讲课,但人太多坐后面,基本听不到重…是你的核心竞争力。想要得到用户的认同,你就必须做到产品质量好
旺道:实体店怎么做网络营销?
年纪大了,体力不好了,便赚不了什么钱了。所以,一定要在年轻的…写文案都是非常头痛的事情,那么下面我会给大家提供一个解决方案。在互联网营销系统里面有很多细节营销,软文营销就是其中一种,但好多小白头痛写文章,所以可以用这个方法也一样可以植入广告语,或者是图片广告,轮显切换广告,使用 旺道营销系统,也就是每…以帮你吸引海量精准粉丝。有了粉丝,你通过广告、产品、服务就可
旺道:圈里圈外,铁饭碗距离瓷饭碗究竟有多远?
中国人爱混圈子,从线下一直混到线上,各种奇形怪状的圈子勾勒出…不能推则敷衍了事,盼着混到主任科员退休;一种是老黄牛式,踏踏实实、兢兢业业工作,但拙于人际关系,活干得最多,但依旧不见晋升迹象;还有一种是提线木偶式,你扯一下线他动一下,好像脑子和身体是分离的。我突然一拍脑袋,这是提前衰老的迹象啊,工作内容…即使选择留在铁饭碗圈里,也应保有随时离开的思想警惕和进行必要
旺道:微信要双开了,朋友圈推广更好做了
现在手机只能登一个微信,如两个,三个微信的小伙伴们就只能退出…动推广工具、 设置头像点击头像图标,点“+”选择图片,可以上传你的企业,个人头像,产品服务等。旺道微信朋友圈广告免费朋友圈广告-旺道、 设置推广主题点“广告”,再点“+”设置推广的主题(类别),填好中文主题,以及你的网址。教你不花一分钱做朋…简直就是一劳永逸哟。如果你点开发布的文章看不到广告,那就是你
旺道:如何能让网站多个关键词排名百度首页?
最近接了一个单,做婴儿游泳馆加盟的。优化推广范围是全国,但是…泳馆加盟哪家好”、“婴儿游泳加盟多少钱”这类与我息息相关的词。如何能让网站多个关键词排名百度首页?但客户并不认同,客户说这个词指数低,或者说搜的人不多。是,这是一方面原因。但是,指数低有什么关系,你是卖产品还是卖指数。搜的人不多有什么关系,…实现广告效果最大化,来满足大众化的广告需求。对于这样的四两拔
旺道:如何成为网络营销达人?这个技能帮你节省80%的时间
在某次名师讲坛上,听到了一个让人警醒的故事。说有个村子缺水,…准的产品。现在已经不是新鲜事物,好多企业因为百度竞价的高额费用,头疼不已,所以做网站关键词优化排名,成为了企业必选的推广项目。做优化也要做到有成效,不然就是一分钱也是昂贵的。用 旺道智能机器人 就可以持续代替人工去做,它非常聪明,只要你够耐…手,设备先进,你多努力奔跑也于事无补。简单说,要去一个地方,

此文章来源来互联网,如果你有任何版权冲突,可以联络我们,微信号:WANCOME,QQ:1444641。加我们时,请说明来意,我们将优先处理你的问题。




商弈云推  | 圈广告  | 跨圈推  | 圈推广  | 圈营销
粤ICP备10213132号
旺道商标注册证号:8608864
Copyright © 2016 环企网络信息科技有限公司 版权所有