Delphi内存管理与内存泄漏探析

Delphi内存管理与内存泄漏探析

杨继宏,龚 晖,李 治

   :综述了Delphi环境下动态内存分配与释放的方法,分析了内存泄漏的可能原因,并列举了开发“智能型远程作业系统”过程中出现的有关内存泄漏的几个实例。
    关键词:内存分配;内存释放;内存泄漏;智能型远程作业系统


1  
  Delphi是Borland公司的划时代之作,以其功能强大且易学 好用而受到广大程序员的青睐。关于Delphi的文章很多,大多数是讨论其生产的高效率、各种应用的快速实现,却忽视了一个基本却非常重要的问题内存动态 分配与安全释放。Delphi应用程序开发的许多问题是由不正确的内存分配或释放引起的,如内存未分配、未释放、未初始化、边界覆盖等。尤其是当自己编写 一些组件程序时,稍不留心就会出现所谓“内存泄漏”的问题:某些极端的情况下,当某一问题组件被重复调用时,会大量“吃掉”机器的内存,导致应用程序无法 运行甚至死机的情况,从而严重影响了程序的运行稳定性。因此,内存安全管理是每一个程序员应具备的基本技能,也是高质量运行稳定的应用程序重要标志之一。 开发一个运行稳定高质量的应用,就不可避免的遇到内存动态分配与释放的问题。本文主要就Delphi中内存分配与释放以及可能产生内存泄漏的原因展开讨 论。
2 动态内存分配与释放
Delphi环境下,动态分配内存的方式主要有2种:
第1种是使用Delphi标准库函数:AllocMem(GetMemor ReAllocMem,FreeMem)或New(Dispose)动态分配内存和释放内存;第2种是通过调用Win32 API函数LocalAlloc和LocalFree来实现。
2.1 Delphi标准库函数
    其函数原型如下:
    function AllocMem(Size:Cardinal):Pointer;
    procedure GetMem(var P: Pointer;Size:Integer);
    procedure ReAllocMem(var P:Pointer;Size:Integer);//重新分配内存
    procedure FreeMem(var P:Pointer[;Size:Integer]);
    procedure New(var P:Pointer);
    procedure Dispose(var P:Pointer);
调用AllocMem是向系统请求指定大小的内存的一种方法。如果条件允许,操作系统就会进行指定大小内存的分配,返回值是一个指向内存块的指针。函 数GetMem和ReAllocMem用法类似。区别是AllocMem函数在分配一个内存块的同时将每个字节初始化为0,而GetMem函数则不进行初 始化为0的操作。而ReAllocMem函数则进行内存的重新分配操作。函数New自动给指针P分配SizeOf(P)大小的内存而不需要显式指定申请内 存的大小。
调用函数AllocMem(GetMem,ReAllocMem)申用New分配的内存。
调用AllocMem(GetMem,ReAllocMem)及FreeMem的过程如下:
    (1)首先要包含SysUtils单元文件
    uses SysUtils
(2)在声名了相应类型的指针ptr:^Type后,调用AllocMem函数并指定所需大小的字节数作为参数。

(3)释放内存,调用FreeMem并将指针作为参数,最后将指针置空。
    FreeMem(ptr);ptr:=nil;
另一对内存分配与释放的函数New,Dispose用法与上面类似。
2.2 Win32API函数LocalAlloc和LocalFree
  函数原型如下:

Win32 APILocalAlloc和LocalFree函数只适用于Win32平台,其使用参阅在线Win32 sdk帮助文件。
无论使用哪一种方式向系统申请内存,在使用或释放申请的内存前,应首先测试一下指向内存块的指针是否有效。如果内存申请成功,函数返回的指针包含一有 效地址(一个>0的长整数);否则,将返回一个空指针nil。因此可用if(Ptr=nil)测试指针是否有效。无论使用哪一种方式申请内存,必须使用相 应的函数释放内存。
3 内存泄漏原因分析
3.1 申请的内存没有释放
  这是一个基本的原因。似乎他是完全能够避免的。但是实际情况并非如此。尤其在编写一段比较庞大的程序时,在某个地方可能会申请一些 系统资源,但是在某些情况下可能不需要申请这些资源,所以某些粗心的程序员忘记主动去释放他们。程序可能暂时不会出现异常,但是,如果这个函数被多次调 用,特别是在一个长时间运行的服务程序当中,他就可能会耗尽系统的内存资源,甚至导致主机系统瘫痪。所以有个基本原则就是你一定要释放你所创建的对象,让 Delphi组件去释放他们所创建的任何东西。
3.2 对同一个对象创建了多个实例,但只释放了某一个实例
  我们来看一个例子:在“智能型远程作业系统”中,学生答题表单AnsFrm:TAnsFrm有一个按钮BtnFrmu,点击此按钮 调用“公式编辑器”FrmuEdit:TFrmuEdit,由于程序需要,当“公式编辑器”界面被关闭时,并不释放其实例,直到关闭学生答题表单 AnsFrm时再去释放这个实例。
    TansFrm设置私有变量:FrmuEdit:TFrmuEdit。点击按钮BtnFrmu事件代码:

现在n次点击按钮BtnFrmu,则将创建n个“公式编辑器”实例,然而关闭表单AnsFrm时实际上只释放了最后一个实例,还有(n-1)个实例在内存中,从而造成了内存泄漏。
解决方法是创建新的实例之前先检测是否存在某个实例,如果存在,则不创建。修改按钮BtnFrmu的OnClick事件为:

如果FrmuEdit已经被创建,则Assigned(FrmuEdit)返回真;反之,返回假。现在无论点击按钮BtnFrmu多少次,内存中只有TFrmuEdit的一个实例,从而有效地避免了此种内存泄漏的发生。
3.3 用于释放资源的代码实际上并没有被调用
  开发Delphi组件程序时可能会出现这种情况。Delphi是事件驱动的,事件之间有着各种联系。组件程序开发者一定要十分清楚 Delphi的事件机制,尤其要明白某些事件(消息)之间的先后调用关系。否则,如果你认为事件1一定会调用事件2,所以在事件2中编写了释放内存的代 码;而事实上事件2并不调用(或有时不调用)事件1,则这段释放内存的代码实际上并不起作用,当然要造成内存的泄漏。笔者在开发“智能型远程作业系统”的 “图形/文本混合编辑器”组件程序时就遇到此种情形。
“图形/文本混合编辑器”TEditEx=class(TCustomControl),采用多缓冲技术,TEditEx有私有成员:GraphDC,TextDC,BuffDC:HDC(图形缓冲,文本缓冲和混合缓冲设备环境句柄);设备环境初始化:

注意到TEditEx是从TCustomControl继承的,不是一个窗体类,故其实例被关闭时不会触发WM_CLOSE 消息,即不调用这部分代码去释放申请的资源;也就是说在关闭这个实例后,向系统申请的这部分资源(约2 MB)仍然在内存中。学生在答题时会频繁的用到“图形/文本混合编辑器”,即频繁的创建TEditEx的一个实例,然后关闭他,但是却不释放所申请的资 源。在使用“图形/文本混合编辑器”4~6次(和机器有关)后,系统资源接近枯竭而不得不关闭主程序甚至要重启机器。
    解决方法就是在销毁器中释放这些资源:

3.4 出现异常时没有释放内存
  程序运行时难免发生异常,如果此处恰好存放了释放资源的代码,则申请的资源将得不到释放,从而产生一种内存泄漏。采用Delphi 的异常处理技术,可以确保申请资源的安全释放。Delphi支持如下2种异常处理:try…except和try…finally。其语法分别是:
try statements except exceptionBlock end和trystatementList1 finally statementList2 end。
这2种异常处理是有区别的:当系统资源被分配后,如果希望仅当在出现程序异常时释放资源,一般应使用try…except;如果不管程序在执行程序块 时是否出现异常均希望释放资源,一般使用try…finally。切忌滥用异常处理结构,否则,将人为地降低程序的性能。  申请内存或新建内存对象以 后,将可能产生异常的代码放在try部分,一旦异常发生,无论是try部分的哪一行产生的,程序均会跳转到异常处理代码段(except或finally 部分),在这部分存放释放资源的代码,以保证申请资源的释放。正确使用Delphi提供异常处理技术,无论程序在运行时出现何种异常,都能保证申请资源的 释放。
4  
  内存的动态分配与安全释放,对于应用的稳定、主机系统的安全非常重要。要对程序中各种对象(这里的对象是泛指那些占 用系统资源的变量(包括各种基本类型变量和类实例对象变量等))的生存(包括从分配到释放)有完全清楚的认识,杜绝内存泄漏的发生,开发出健壮、高效的应 用程序。

posted @ 2011-11-10 20:56  Handll  阅读(438)  评论(0编辑  收藏  举报