createbitmap导致的内存泄漏如何处理_C++ 如何避免内存泄漏,一篇就够

前言

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

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

619d7184635aa4c21423d9393cfd2075.png

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

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

内存是如何泄露的

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

ebc7baeebf155c251cf6b2c1d41f0867.png

通常来说,一个线程的栈内存是有限的,通常来说是 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 极大的降低了异步编程的复杂度,降低了内存泄露的可能性,使得编写异步程序像编写同步程序一样简单,降低了程序员心智的开销。

0b1411f2ac507770a61e46ad0160f88f.png

经验 #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。

b308481f9a9d11d6da4564b53e7d6953.png

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 文件,会更直观一些。

8a76f5e6c1e91ae5dad2446b41c305ec.png

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

写在最后

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

如果你在C/C++以及后台服务开发方面有什么困惑的话,我们建了一个QQ群:762073882,谢绝广告。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/259772.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visio绘制功能分解图

为什么要绘制功能分解图? 对于编程人员来说,具体分配任务的时候,必须知道自己要做什么,必须了解系统的大体框架。功能分解图可以帮助我们理清程序的框架,便于大局观的掌握。 用Visio2010创建功能分解图 1、选择模版 2、…

Heka:Go编写,来自Mozilla,高效、灵活的插件式数据挖掘工具(转)

转自:http://www.csdn.net/article/2013-05-02/2815116-introduce-from-mozilla-heka-go摘要:一直崇尚开源的Mozilla近日释放了Heka测试版——插件架构,Go编写。在支持使用Go扩展功能的同时,还通过允许“Sandboxed Filters”提供了…

cocos2d学习笔记2——学习资源

1. 视频 找了好几个视频,有一些讲得好的文件资源没有,后来终于找到一个讲得不错还有文件资源的,还有高清下载地址,虽然是2.2版本的,但是确实能学到不少东西,对用cocos2d做游戏有了基本的印象,对…

环境变量_配置JAVA环境变量

本文标识 : J00001本文编辑 : YiKi编程工具 : IDEA阅读时长 : 3分钟什么是环境变量?环境变量是在操作系统中一个具有特定名字的对象, 它包含了一个或者多个应用程序所将使用到的信息。为什么要配置环境变量?为了方便在控制台编译和运行java程序,不…

分布式消息队列 Kafka

分布式消息队列 Kafka 2016-02-25 杜亦舒Kafka是一个高吞吐量的、分布式的消息系统,由Linkedin开发,开发语言为scala具有高吞吐、可扩展、分布式等特点 适用场景 活动数据统计活动数据包括页面访问量(Page View)、被查看内容方面的…

漫游飞行_手机“飞行模式”为何没被淘汰?内行人坦言:其实是你不会用!

随着科技的不断创新,目前市面上出现的手机款式多种多样,品牌也非常多,有华为、苹果、三星和小米等等。手机的屏幕也是五花八门,有刘海屏、水滴全面屏等,这些屏幕之间都各有不同。而且手机的更新换代速度很快&#xff0…

multiselect多选下拉框

具体实现 <input type"hidden" id"q_dueDay" name"q_dueDay" value"${baseQueryBean.q_dueDay}">//这个为隐藏域后台直接使用这个为参数 <select id"example" name"example" multiple"multiple&qu…

scikit-learn点滴

scikit-learn点滴 scikit-learn是非常漂亮的一个机器学习库,在某些时候,使用这些库能够大量的节省你的时间,至少,我们用Python,应该是很难写出速度快如斯的代码的. scikit-learn官方出了一些文档,但是个人觉得,它的文档很多东西都没有讲清楚,它说算法原理的时候,只是描述一下,除…

怎样搭建Android开发平台(转)

Android是基于Linux内核的软件平台和操作系统&#xff0c;是Google在2007年11月5日公布的手机系统平台&#xff0c;早期由Google开发&#xff0c;后由开放手机联盟&#xff08;Open Handset Alliance&#xff09;开发。 它采用了软件堆层&#xff08;software stack&#xff0c…

subject.login(token)是如何确认账号密码的_教你如何删除、关闭、注销微信小程序...

微信小程序是我们日常生活中经常会接触到的工具&#xff0c;打开小程序后&#xff0c;它就会留在我们微信的”“发现-小程序”栏。很多人并不知道该如何删除、关闭小程序&#xff0c;所以今天就跟大家科普下相关问题。1.如何删除小程序首先&#xff0c;打开微信界面&#xff0c…

上海交通大学2006年数学分析考研试题

转载于:https://www.cnblogs.com/zhangzujin/p/4078900.html

出现的是乱码_cad状态栏出现了方框乱码怎么办?

左下角阅读原文看CAD视频好课推荐&#xff1a;1、CAD2014&#xff1a;点击查看 2、室内&全屋&#xff1a;点击查看 3、CAD2019&#xff1a;点击查看4、CAD2018&#xff1a;点击查看5、Bim教程&#xff1a;点击查看6、室内手绘&#xff1a;点击查看7、CAD三维&#xff1a;点…

UILabel 详解

UILabel 多行文字自动换行 &#xff08;自动折行&#xff09;1.UIView *footerView [[UIView alloc] initWithFrame:CGRectMake(10, 100, 300, 180)]; 2. UILabel *label [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 300, 150)]; 3. label.text "…

onclick实现超链接_给超链接加onclick事件

在动态网页中&#xff0c;常常需要在单击超链接时处理一些数据&#xff0c;而不是跳转一个网页。在这种情况下&#xff0c;通常有以下三种处理方式&#xff1a;不设置标签的href属性&#xff0c;只设置onclick属性。在这种处理方式下&#xff0c;通常超链接文本会和正文的文本以…

Android 布局以及优化资料汇总

2019独角兽企业重金招聘Python工程师标准>>> 1.性能优化之布局优化 2.Android 开源库 V - Layout 转载于:https://my.oschina.net/zhugenqiang/blog/822942

mysql字符集排序规则_Mysql 字符集及排序规则

一、字符集字符集&#xff1a;就是用来定义字符在数据库中的编码的集合。常见的字符集&#xff1a;utf8、Unicode、GBK、GB2312(支持中文)、ASCCI(不支持中文)二、字符集排序规则作者本人用的是utf8_general_ci后缀ci (case insensitive)意味不区分大小写(大小写不敏感)&#x…

驱动06.触摸屏驱动程序

1.触摸屏的简介 触摸屏是标准的输入设备&#xff0c;在写驱动程序时采用的之前讲过的输入子系统那套框架。我们无需关心对设备文件的操作&#xff0c;只需关心对硬件寄存器的操作和上报事件即可。 触摸屏是附在LCD上的一层薄膜&#xff0c;并不是我们平时认识的触摸屏&#xff…

2016国产开源软件Top100(Q1)

2016国产开源软件Top100(Q1) 随着互联网的发展、开放标准的普及和虚拟化技术的应用等诸多IT新领域的创新及拓展&#xff0c;开源技术凭借其开放性、低成本、稳定性、灵活性、安全性和技术创新性等特点迅速走向成熟&#xff0c;逐步发展成为一种主流模式&#xff0c;日益改变着全…

mysql lenenc int_MySQL-NonMySQL同步工具源码解读——确定同步位置

经过上一节的鉴权过后&#xff0c;程序已经受主库认可&#xff0c;并且可以像主库发起同步请求。在发起请求之前&#xff0c;还有一个可选的步骤&#xff1a;确认同步时间点。同步时间点由两个属性进行标识&#xff1a;Binlog文件名、偏移量。工具支持自定义时间点&#xff0c;…

DancingLinks刷题集

HDU 3663 Power Stations 精确覆盖 题意&#xff1a;每个城市i有xi->yi天可以成为发射站&#xff0c;发射站覆盖范围为与该站有一条边链接的城市。 同时&#xff0c;每个每天城市必须且只能被一个发射站覆盖 天数D<5。 每个城市的发射站关闭后就不再开启。即只能选择一段…