在计算机科学中,内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是
应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏与许多其他问题有着相似的症状,并且通常情况下只能由那些可以获得程序
源代码的
程序员才可以分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,严格意义上来说这是不准确的。
一般我们常说的内存泄漏是指
堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。应用程序一般使用malloc,calloc,realloc等函数(C++中使用new操作符)从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
1.
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以
测试环境和测试方法对检测内存泄漏至关重要。
3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在一个Singleton类的
构造函数中分配内存,在
析构函数中却没有释放该内存。而Singleton类只存在一个实例,所以内存泄漏只会发生一次。
4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了
堆内存发生泄漏的情形:
void MyFunction(int nSize)
{
char* p= new char[nSize];
if( !GetStringFrom( p, nSize ) ){
MessageBox(“Error”);
return;
}
…//using the string pointed by p;
delete[] p;
}
当函数GetStringFrom()返回零的时候,
指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是
c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。
内存泄漏会因为减少可用内存的数量从而降低计算机的性能。最终,在最糟糕的情况下,过多的可用内存被分配掉导致全部或部分设备停止正常工作,或者
应用程序崩溃。
内存泄漏可能不严重,甚至能够被常规的手段检测出来。在现代
操作系统中,一个应用程序使用的
常规内存在程序终止时被释放。这表示一个短暂运行的应用程序中的内存泄漏不会导致严重后果。
在以下情况,内存泄漏导致较严重的后果:
* 程序运行后置之不理,并且随着时间的流失消耗越来越多的内存(比如服务器上的
后台任务,尤其是
嵌入式系统中的后台任务,这些任务可能被运行后很多年内都置之不理)
* 新的内存被频繁地分配,比如当显示电脑游戏或动画视频画面时
* 程序能够请求未被释放的内存(比如
共享内存),甚至是在程序终止的时候
* 泄漏在操作系统内部发生
* 泄漏在系统关键驱动中发生
* 内存非常有限,比如在嵌入式系统或便携设备中
* 当运行于一个终止时内存并不自动释放的操作系统(比如AmigaOS)之上,而且一旦丢失只能通过重启来恢复。
以下例子无需任何程式设计上的知识,但能表明如何导致
内存泄漏及其造成的影响。注意以下的例子是虚构的。
在此例中的应用程式是一个简单
软件的一小部分,
FreeEIM用来控制
升降机的运作。此部分软件当乘客在升降机内按下一楼层的按钮时运行。
当按下按钮时:
1.得到内存,用作记住目的楼层
2.把目的楼层的数字存储到内存
3.升降机是否已到达目的楼层?
如是,没有任何事需要做:程式完成
否则:
(1).等待直至升降机停止
(2).到达指定楼层
(3).把刚才用作记住目的楼层的内存释出。
此程式有一处会造成内存泄漏。如果在升降机所在楼层按下该层的按钮,内存就会一直被占用而不再释放。这种情况发生得越多,泄漏的内存则越多。
这个小错误不会造成即时影响。因为人不会经常在升降机所在楼层按下同一层的按钮。而且在通常情况下,升降机应有足够的内存以应付上百次、上千次类似的情况。不过,升降机最后仍有可能消耗完所有内存。这可能需要数个月或是数年,所以在简单的测试下这个问题不会被发现。
而这个例子导致的后果会是不那么令人愉快。至少,升降机不会再响应前往其他楼层的要求。更严重的是,如果程式需要内存去开启升降机门,那可能有人被困升降机内,因为升降机没有足够的内存去开启升降机门。
内存泄漏只会在程式运行的时间内持续。例如:关闭升降机的电源时,程式终止运行。当电源再度开启,程式会再次运行而内存会重置,而这种缓慢的泄漏亦会从头开始再次发生。
内存泄漏是程式设计中一项常见错误,特别是使用没有内置自动
垃圾回收的
编程语言,如
C及
C++。一般情况下,内存泄漏发生是因为不能存取动态分配的内存。目前有相当数量的调试工具用于检测不能存取的内存,从而可以防止内存泄漏问题,如IBM Rational Purify、BoundsChecker、Valgrind、
Insure++及memwatch都是为C/C++程式设计亦较受欢迎的内存除错工具。
飞鸽传书垃圾回收则可以应用到任何编程语言,而C/C++也有此类函式库。
提供自动
内存管理的编程语言如
Java、
VB、
.NET(
.Net内存泄露)以及
LISP,都不能避免内存泄漏。例如,程式会把项目加入至列表,但在完成时没有移除,如同人把物件丢到一堆物品中或放到抽屉内,但后来忘记取走这件物品一样。
内存管理器不能判断项目是否将再被存取,除非程式作出一些指示表明不会再被存取。
虽然内存管理器可以回复不能存取的内存,但它不可以释放可存取的内存因为仍有可能需要使用。现代的内存管理器因此为程式设计员提供技术来标示内存的可用性,以不同级别的“存取性”表示。内存管理器不会把需要存取可能较高的对象释放。当对象直接和一个强引用相关或者间接和一组强引用相关表示该对象存取性较强。(强引用相对于弱引用,是防止对象被回收的一个引用。)要防止此类内存泄漏,开发者必须使用对象后清理引用,一般都是在不再需要时将引用设成null,如果有可能,把维持强引用的事件侦听器全部注销。
一般来说,
自动内存管理对开发者来讲比较方便,因为他们不需要实现释放的动作,或担心清理内存的顺序,而不用考虑对象是否依然被引用。对开发者来说,了解一个引用是否有必要保持比了解一个对象是否被引用要简单得多。但是,自动内存管理不能消除所有的内容泄漏。
如果一个程序存在内存泄漏并且它的内存使用量稳定增长,通常不会有很快的症状。每个
物理系统都有一个较大的内存量,如果内存泄漏没有被中止(比如重启造成泄漏的程序)的话,它迟早会造成问题。
大多数的现代计算机
操作系统都有存储在RAM芯片中主内存和存储在次级存储设备如硬盘中的
虚拟内存,
内存分配是动态的——每个进程根据要求获得相应的内存。存取活跃的
页面文件被转移到主内存以提高存取速度;反之,存取不活跃的页面文件被转移到次级存储设备。当一个简单的进程消耗大量的内存时,它通常占用越来越多的主内存,使其他程序转到次级存储设备,使系统的运行效率大大降低。甚至在有内存泄漏的程序终止后,其他程序需要相当长的时间才能切换到主内存,恢复原来的运行效率。
当系统所有的内存全部耗完后(包括主内存和虚拟内存,在
嵌入式系统中,仅有主内存),所有申请内存的操作将失败。这通常导致程序试图申请内存来终止自己,或造成分段内存访问错误(segmentation fault)。现在有一些专门为修复这种情况而设计的程序,常用的办法是预留一些内存。值得注意的是,第一个遭遇得不到内存问题的程序有时候并不是有内存泄漏的程序。
一些
多任务操作系统有特殊的机制来处理内存耗尽得情况,如随机终止一个进程(可能会终止一些正常的进程),或终止耗用内存最大的进程(很有可能是引起
内存泄漏的进程)。另一些操作系统则有内存分配限制,这样可以防止任何一个进程耗用完整个系统的内存。这种设计的缺点是有时候某些进程确实需要较大数量的内存时,如一些处理图像,视频和科学计算的进程,操作系统需要重新配置。
如内存泄漏发生在
内核,表示操作系统自身发生了问题。那些没有完善的
内存管理的计算机,如嵌入式系统,会因为一个长时间的内存泄漏而崩溃。
一些被公众访问的系统,如
网络服务器或
路由器很容易被
黑客攻击,加入一段攻击代码,而产生内存泄漏。
值得注意的是,内存用量持续增加不一定表明内存泄漏。一些应用程式会储存越来越多资料到内存中(如用作快取。如果快取太大引起问题,这可能是程式设计上的错误,但并非是内存泄漏因为资料仍被使用。另一方面,程式有可能申请不合理的大量内存因为程式设计者假设内存总是足够运行特定的工作;例如,图像档案处理器会在开始时阅读图像档案的内容并把之储存至内存中,有时候由于图像档案太大,消耗的内存超过了可用的内存导致失败。
另一角度讲,内存泄漏是一种特殊的编程错误,如果没有
源代码,根据征兆只能猜测可能有内存泄漏。在这种情况下,使用术语“内存消耗持续增加”可能更确切。
检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的
指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。这里只是简单的描述了检测内存泄漏的基本原理,详细的算法可以参见Steve Maguire的<<Writing Solid Code>>。
如果要检测
堆内存的泄漏,那么需要截获住malloc/realloc/free和new/delete就可以了(其实new/delete最终也是用malloc/free的,所以只要截获前面一组即可)。对于其他的泄漏,可以采用类似的方法,截获住相应的分配和释放函数。比如,要检测BSTR的泄漏,就需要截获SysAllocString/SysFreeString;要检测HMENU的泄漏,就需要截获
CreateMenu/ DestroyMenu。(有的资源的分配函数有多个,释放函数只有一个,比如,SysAllocStringLen也可以用来分配BSTR,这时就需要截获多个分配函数)
在Windows平台下,检测内存泄漏的工具常用的一般有三种,MS C-Runtime Library内建的检测功能;外挂式的检测工具,诸如,Purify,BoundsChecker等;利用Windows NT自带的Performance Monitor。这三种工具各有优缺点,MS C-Runtime Library虽然功能上较之外挂式的工具要弱,但是它是免费的;Performance Monitor虽然无法标示出发生问题的代码,但是它能检测出隐式的内存泄漏的存在,这是其他两类工具无能为力的地方。
编辑本段内存泄露常见的几种原因
1、对于通过new等
运算符申请到的内存空间在使用之后没有释放掉。关于这个问题,如果是在过程程序中开辟的空间,可以在过程结束时释放;但是如果是
面向对象的编程,在类的
构造函数中开辟的空间,那么记得一定要在
析构函数中释放,但是如果析构函数出现问题了,导致不能释放内存空间,就造成了内存泄露。
2、对于程序中的windows句柄使用完要close掉。
3、对于内存的泄露有的时候是忘记了回收,但是有的时候是无法回收,比如1中提到的析构函数不正确导致内存泄露,这是属于程序有问题;还有关于面向对象编程的一个内存泄露的可能性:一个对象在构造函数中
抛出异常,对象本身的内存会被成功释放,但是其析构函数不会被调用,其内部
成员变量都可以成功析构,但是用户在构造函数中动态生成的对象无法成功释放。如果一个对象在构造函数中打开很多
系统资源,但是构造函数中后续代码抛出了异常,则这些资源将不会被释放,建议在构造函数中加入try catch语句,对先前申请的资源进行释放后(也就是做析构函数该做的事情)再次抛出异常,确保内存和其他资源被成功回收。也就是说构造函数出现问题会导致构造函数中开辟的内存空间不能回收,对于对象本身的内存空间还是可以回收的。
#include<malloc.h>
#define LEN sizeof(struct student)
#define NULL 0
struct student
{
long num;
float score;
struct student *next;
};
int n;
struct student*creat(void)
{
struct student *head;
struct student *p1,*p2;
n=0;
p1=p2=(struct student*)malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
head=NULL; //这句可以不要么?不可以!不要这句话,就可能造成内存泄露
while(p1->num!=0)
{
n=n+1;
if(n==1)head=p1;
else p2->next=p1;
p2=p1;
p1=(struct student*)malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
}
p2->next=NULL;
return(head);
}