关于编程中的内存泄露问题的探索
我一直以来很这个内存泄露问题有着自己的担心与探索,现略谈一下我自己的体会。
我个人认为,一个好的编程的习惯(甚至可以提高的水平的高度)就是应该对程序的运行最好作到心中有数。这里的心中有数就是说,你对程序中各种对象(这里的对象是泛指那些占有系统资源的变量(包括各种基本类型变量和类实例对象变量等))的生存(包括从分配到释放)有完全清楚的认识,知道编译器是如何生成他们、释放他们,这中间有两个层次。
第一层是你对你所使用的语言、工具对内存的管理应该了如指掌,应试图守她的规矩、当老实人,比方说既然在C语言中你用malloc分配了一片内存空间,你应该在适当(我个人认为应在不在使用它的最内层程序块)的时候用free去释放它,至于语言所提供的内存管理函数、例程是否能够作到完美无缺(也就是说,free能否真消除malloc的所有影响)那是第二层应该考虑的事情了。
举个例子,函数中的局部变量一般编译器的实现是这样:在调用者的栈空间临时分配空间,随着函数的返回,一般都会退栈释放这些临时变量所占的存储空间(比如说在x86平台下的带参数的返回指令 RET n就有利于编译器实现这一功能)。当了解了这些背景知识后,你就可以心安理得地放心使用这些局部变量而不担心释放他们了!
(关于堆)
但是对于动态的存储分配就不这么简单了,试想你在C语言中用malloc函数分配了指定字节数量的内存,那么编译器(甚至是操作系统)都干了些什么呢?就我的理解,C的库函数malloc一般都是通过在程序运行的堆空间来分配空间而实现。就Win32平台而言,操作系统(就是Windows!)显式地提供了堆管理(分配、释放)API函数支持(如:HeapAlloc, GetProcessHeap, HeapCreate, HeapDestroy, HeapFree, HeapReAlloc, HeapSize等),这些函数给我们如下信息:在进程(我们可以理解为运行中的程序)的地址空间(是一个逻辑地址空间概念,以后我会专门解释)存在一个叫堆的内存空间可供程序使用(实际上,Win32为每个进程保留了一个缺省堆,其尺寸可以在程序的链接过程中通过参数来改变。另外还可创建新的供本进程使用的堆,正如API函数HeapCreate所作的工作那样),C库函数malloc、free就在内部通过调用系统提供的API堆管理函数为程序提供内存分配、释放。那Win32是如何能够做到释放的完整无误的呢,这个以后再讨论(但显然应该为每次内存分配予以记帐,这样就能在释放空间时做到准确(不会多也不会少释放空间))。其它操作系统也通过相应的系统功能调用来提供类似的功能。
(关于内存管理)
那么操作系统是如何管理进程的存储空间甚至其他系统资源(如代表打开的文件对象的句柄)的呢?因为这个问题不搞清楚,你就会有疑虑:我的程序运行结束后,如果操作系统不能完全释放本程序所占有的资源(泛指存储空间及其它系统资源,下同)?那么我们的程序多次运行,或其他具有类似问题的应用程序的经常运行就会逐渐耗尽系统的资源,从而导致系统不得不重启。从这里也可以看出,一个操作系统能够多长时间不需重启能够反映出该操作系统在资源管理方面的能力,是系统健壮性的重要体现,因为尽管程序员们在程序设计时程序力图避免未释放的资源,但总不免有不是做得很好的,而且这种可能几乎是必然的,因此在操作系统设计时就应该对进程资源管理进行重点考虑,怎么又扯到操作系统的设计上去了,跑题了!那么我们来看一下Win32对进程占用资源的管理,关键是考察系统在正常或非正常结束一个进程所做的,就会做到心中有数了。
我们知道,现代操作系统几乎都运行保护模式下,保护模式的一个基本特征就是存储保护,存储保护的基本目标就是使各个进程的地址空间相互隔离,从而一个进程不能看见其它进程的数据(当然,为了共享目的而设立的共享存储区除外),其基本实现机制是分段、分页。分段使得各个进程运行在存储器的不同部分,对于越界的访问企图都被CPU硬件予以拒绝。分页的基本功能使得各个进程的地址空间的页(一种固定大小内存块,在x86平台上是4K)通过CPU硬件透明地映射到不同的物理内存页帧,从而实现自动隔离。考虑到各种平台的可移植性,Windows系统采用分页而非分段的平板式的内存管理模型,这种模型有一个好处,使得对于象C这种支持指针的语言给程序员提供一个连续的内存空间印象。无论是分段还是分页,都给操作系统提供了的内存回收提供了便利条件,操作系统只需重置描述该进程的段或页的信息就可以释放掉该进程所占用的内存空间资源而为以后的进程再利用,当然考虑到内存碎片的问题,内存的回收在实际操中可能更为复杂一些。总之一句话,操作系统要释放一个进程所占有的内存空间是能够做到的。
(关于系统其它资源管理)
还有一个问题就是进程在运行中还要用到操作系统提供的其它资源对象(如文件),操作系统能不能回收这些资源呢。谨慎的程序员会在程序设计时可能不会忘记显式的通知(通过相应的释放系统功能调用)操作系统本进程将不在占用该资源,操作系统应该作相应的处理。但情况并不总是这样乐观,粗心的程序员或者是程序异常终止(比方说出现问题被操作系统给kill掉)总是不能正常释放所占有的这些系统资源。因此,操作系统通过记帐进程所使用的系统资源,在进程结束时一一查看这些资源,予以强制释放。
以Win32平台为例,它为每个进程维护一个句柄表,这些句柄包含指向相应系统资源对象的指针,在进程终止时,操作系统的进程终止进程查看这个句柄表,强制释放进程已打开的的系统资源。可以通过如下一个实例来测试:创建一个进程,该进程打开一个文件对象(设置为非删除共享方式),该进程进入死循环,这时你在资源管理器中删除刚才被打开的文件就会失败,接着你通过kill进程的方式来终止该进程,再删除该文件成功。这个实例说明Win32在进程非正常终止时的确能够释放该进程所占有的系统资源。
能够很好做到第一层的程序员就已经很优秀了!
第二层就是,你对编译器、操作系统的内存管理有相当了解,但又没有透彻了解,从而怀疑他们的管理能力;或者你知道了它的实现详细算法,发现了潜在的缺点(这种可能性似乎不大耶!),于是你有点不放心起来,想:我能不能申请一片内存,通过我自己来管理呢,毕竟我自己管理我自己心里最清楚、最放心,答案是肯定的,起码Win32平台就为你提供了这种可能性,请参阅有关Win32 API(VirtualAlloc,VirtualFree等)。这种另起炉灶的思想在某些情况下是很有用的,比方说要设计长时间运行的服务程序时;另外在Windows内核驱动程序设计时对非分页内存空间的使用,Microsoft公司就鼓励这种方式。不过对这种做法,很多人持有不同见解,认为这破坏了分层思想,不符合软件也积木化的发展潮流。我个人认为还是首先应做到第一层,但一个不“安分”的程序员似乎应该做到第二层,我现在就养成这样一个坏毛病,就是老是想另起炉灶!
以上只是我这几年编程时对内存泄露问题担心、探知的概略,欢迎有兴趣的与我讨论:binaryhead@163.com