C++内存泄漏和内存碎片的产生及避免策略

1.内存泄漏的定义

 

   一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用mallocreallocnew等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用freedelete释放该 内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

 

2、内存泄漏的后果

 

程序运行后置之不理,并且随着时间的流失消耗越来越多的内存(比如服务器上的后台任务,尤其是嵌入式系统中的后台任务,这些任务可能被运行后很多年内都置之不理);

新的内存被频繁地分配,比如当显示电脑游戏或动画视频画面时;

程序能够请求未被释放的内存(比如共享内存),甚至是在程序终止的时候;

泄漏在操作系统内部发生;

泄漏在系统关键驱动中发生;

内存非常有限,比如在嵌入式系统或便携设备中;

当运行于一个终止时内存并不自动释放的操作系统(比如AmigaOS)之上,而且一旦丢失只能通过重启来恢复。

 

3、如何发现内存泄漏

 

有些简单的内存泄漏问题可以从在代码的检查阶段确定。还有些泄漏比较严重的,即在很短的时间内导致程序或系统崩溃,或者系统报告没有足够内存,也比较容易发现。最困难的就是泄漏比较缓慢,需要观测几天、几周甚至几个月才能看到明显异常现象。那么如何在比较短的时间内检测出有没有潜在的内存泄漏问题呢?实际上不同的系统都带有内存监视工具,我们可以从监视工具收集一段时间内的堆栈内存信息,观测增长趋势,来确定是否有内存泄漏。在 Linux 平台可以用 ps 命令,来监视内存的使用,比如下面的命令 (观测指定进程的VSZ)

ps -aux

静态分析,包括手动检测和静态工具,是代价最小的方法。

1/当使用 C/C++ 进行开发时,采用良好的一致的编程规范是防止内存问题第一道也是最重要的措施。检测是编码标准的补充。二者各有裨益,但结合使用效果特别好。专业的 或 C++ 专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过少量的实践和适当的文本搜索,您能够快速验证平衡的 *alloc() 和 free() 或者 new 和 delete 的源主体。人工查看此类内容通常会出现像清单 中一样的问题,可以定位出在函数 LeakTest 中的堆变量 Logmsg 没有释放。

 

2/代码静态扫描和分析的工具比较多,比如 splint, PC-LINT, BEAM 等。因为 BEAM 支持的平台比较多,这以 BEAM 为例,做个简单介绍,其它有类似的处理过程。

BEAM 可以检测四类问题没有初始化的变量;废弃的空指针;内存泄漏;冗余计算。而且支持的平台比较多。

BEAM 支持以下平台:

Linux x86 (glibc 2.2.4)

Linux s390/s390x (glibc 2.3.3 or higher)

Linux (PowerPC, USS) (glibc 2.3.2 or higher)

AIX (4.3.2+)

Window2000 以上

 

或者使用内嵌程序自动检测

可以重载内存分配和释放函数 new 和 delete,然后编写程序定期统计内存的分配和释放,从中找出可能的内存泄漏。或者调用系统函数定期监视程序堆的大小,关键要确定堆的增长是泄漏而不是合理的内存使用。这类方法比较复杂,在这就不给出详细例子了。

 

2、动态运行检测

实时检测工具主要有 valgrind, Rational purify 等。

1、 Valgrind

valgrind 是帮助程序员寻找程序里的 bug 和改进程序性能的工具。程序通过 valgrind 运行时,valgrind 收集各种有用的信息,通过这些信息可以找到程序中潜在的 bug 和性能瓶颈。

Valgrind 现在提供多个工具,其中最重要的是 MemcheckCachegrindMassif 和 CallgrindValgrind 是在 Linux 系统下开发应用程序时用于调试内存问题的工具。它尤其擅长发现内存管理的问题,它可以检查程序运行时的内存泄漏问题。其中的 memecheck 工具可以用来寻找 cc++ 程序中内存管理的错误。可以检查出下列几种内存操作上的错误:

读写已经释放的内存

读写内存块越界(从前或者从后)

使用还未初始化的变量

将无意义的参数传递给系统调用

内存泄漏

 2Rational purify

Rational Purify 主要针对软件开发过程中难于发现的内存错误、运行时错误。在软件开发过程中自动地发现错误,准确地定位错误,提供完备的错误信息,从而减少了调试时间。同时也是市场上唯一支持多种平台的类似工具,并且可以和很多主流开发工具集成。Purify 可以检查应用的每一个模块,甚至可以查出复杂的多线程或进程应用中的错误。另外不仅可以检查 C/C++,还可以对 Java 或 .NET 中的内存泄漏问题给出报告。

在 Linux 系统中,使用 Purify 需要重新编译程序。通常的做法是修改 Makefile 中的编译器变量。下面是用来编译本文中程序的 Makefile

CC=purify gcc

首先运行 Purify 安装目录下的 purifyplus_setup.sh 来设置环境变量,然后运行 make 重新编译程序。

./purifyplus_setup.sh

下面给出编译一个代码文件的示例,源代码文件命名为 test3.cpp. 用 purify 和 g++ 的编译命令如下,‘-g’是编译时加上调试信息。

purify g++ -g test3.cpp o test

运行编译生成的可执行文件 test,就可以得到图1,可以定位出内存泄漏的具体位置。

./test

 

4、如何避免内存泄漏

其实内存泄漏的原因可以概括为:调用了malloc/new等内存申请的操作,但缺少了对应的free/delete,总之就是,malloc/newfree/delete的数量多。我们在编程时需要注意这点,保证每个malloc都有对应的free,每个new都有对应的deleted!!!平时要养成这样一个好的习惯。

 

要避免内存泄漏可以总结为以下几点:

 

1、程序员要养成良好习惯,保证malloc/newfree/delete匹配;

2、一遍又一遍的看代码,希望能看出内存泄露的bug 

3、检查malloc/newfree/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。

4.、总结了好些规律:要成对使用,free掉的内存指针要置空,在尽量少的函数中申请和释放内存,等等 

5、使用了大量的内存村泄露检测工具,结合各种自动测试软件,采取疯狂加变态的测试方法,试图找出所有可能的内存泄露bug。 

  6、代码规模超过千万行,内存从一个模块被传递到另一个或多个,谁知道传递给哪个模块,最后在那里释放的了,反正我保证那个指针指向的数据是好的就行了。由于代码的规模,穷举式的测试根本不可能,只有一遍一遍的看代码,祈祷自己的代码没有内存泄露的问题。 

 

 

内存碎片

 

1、什么是内存碎片

   

    内存碎片---描述一个系统中所有的不可用的空闲内存;这些资源之所以仍然未被使用,是因为负责分配内存的分配器使这些内存无法使用。这一问题通常都会发生,原因在于空闲内存以小而不连续方式出现在不同的位置。由于分配方法决定内存碎片是否是一个问题,因此内存分配器在保证空闲资源可用性方面扮演着重要的角色。

 

2、内存碎片产生的原因

 

    原因在与空闲内存以小而不连续的方式出现在不同的位置(内存分配较小,并且分配的这些小的内存生存周期又较长,反复申请后将产生内存碎片的出现)。内存分配程序浪费内存的基本方式有三种:即额外开销、内部碎片以及外部碎片(图 1)。内存分配程序需要存储一些描述其分配状态的数据。这些存储的信息包括任何一个空闲内存块的位置、大小和所有权,以及其它内部状态详情。一般来说,一个运行时间分配程序存放这些额外信息最好的地方是它管理的内存。内存分配程序需要遵循一些基本的内存分配规则。例如,所有的内存分配必须起始于可被 4或 16 整除(视处理器体系结构而定)的地址。内存分配程序把仅仅预定大小的内存块分配给客户,可能还有其它原因。当某个客户请求一个 43 字节的内存块时,它可能会获得 44字节、48字节 甚至更多的字节。由所需大小四舍五入而产生的多余空间就叫内部碎片。

  外部碎片的产生是当已分配内存块之间出现未被使用的差额时,就会产生外部碎片。例如,一个应用程序分配三个连续的内存块,然后使中间的一个内存块空闲。内存分配程序可以重新使用中间内存块供将来进行分配,但不太可能分配的块正好与全部空闲内存一样大。倘若在运行期间,内存分配程序不改变其实现法与四舍五入策略,则额外开销和内部碎片在整个系统寿命期间保持不变。虽然额外开销和内部碎片会浪费内存,因此是不可取的,但外部碎片才是嵌入系统开发人员真正的敌人,造成系统失效的正是分配问题。

 

3、内存碎片的弊端与优点

 

    缺点:

 

    大量的内存碎片会使系统缓慢,原因在于虚拟内存的使用会使内存与硬盘之间的数据交换称为系统

 

缓慢的根源,最终造成内存的枯竭!

 

   优点:

 

   减少内存碎片,提高分配速度,便于内存管理,防止内存泄露

 

4、如何避免内存碎片的产生

  

   1,少用动态内存分配的函数(尽量使用栈空间)

 

   2,分配内存和释放的内存尽量在同一个函数中

 

   3,尽量一次性申请较大的内存2的指数次幂大小的内存空间,而不要反复申请小内存(少进行内存的分割)

 

   4,使用内存池来减少使用堆内存引起的内存碎片

   5、尽可能少地申请空间。

   6尽量少使用堆上的内存空间~

   7做内存池,也就是自己一次申请一块足够大的空间,然后自己来管理,用于大量频繁地new/delete操作。

 

   内存管理系统将能够急时合并相邻空闲内存块,得到更大的空闲内存。这样并不会导致内存碎片的出现。即使相邻空间不空闲,这样产生的碎片还是比较少的,但是对于游戏(运行时间较长)或者手机(内存较小)

你可能感兴趣的:(C++)