FastMM使用详解

一、引言
      FastMM 是适用于delphi的第三方内存管理器,在国外已经是大名鼎鼎,在国内也有许多人在使用或者希望使用,就连 Borland 也在delphi2007抛弃了自己原有的饱受指责的内存管理器,改用FastMM.
      但是,内存管理的复杂性以及缺乏 FastMM 中文文档导致国内许多人在使用时遇到了许多问题,一些人因此而放弃了使用,我在最近的一个项目中使用了FastMM,也因此遇到了许多问题,经过摸索和研究,终于解决了这些问题。
二、为什么要用FastMM
第一个原因是FastMM的性能接近与delphi缺省内存管理器的两倍,可以做一个简单的测试,运行下面的代码:
var
I: Integer;
Tic: Cardinal;
S: string;
begin
tic := GetTickCount;
try
    for I := 0 to 100000 do
    begin
      SetLength(S, I + 100);
      edt1.Text := S;
    end;
finally
    SetLength(S, 0);
    tic := GetTickCount - Tic;
    MessageDlg('Tic = ' + IntToStr(Tic), mtInformation, [mbOK], 0);
end;
end;
在我的IBM T23笔记本上,使用FastMM4(FastMM的最新版本)用时约为3300ms,而使用缺省的内存管理器,用时约为6200ms,FastMM4的性能提高达88%.
第二个原因FastMM的共享内存管理器功能使用简单可靠。当一个应用程序有多个模块(exe和dll)组成时,模块之间的动态内存变量如string的传递就是一个很大的问题,缺省情况下,各个模块都由自己的内存管理器,由一个内存管理器分配的内存也必须在这个内存管理器才能安全释放,否则就会出现内存错误,这样如果在一个模块分配的内存在另外一个模块释放就会出现内存错误。解决这个问题就需要使用到共享内存管理器,让各个模块都使用同一个内存管理器。Delphi缺省的共享内存管理器是BORLNDMM.DLL,这个内存管理器并不可靠,也常常出现问题,并且,在程序发布的时候必须连同这个DLL一起发布。而FastMM的共享内存管理器功能不需要DLL支持,并且更加可靠。
第三个原因是FastMM还拥有一些帮助程序开发的辅助功能,如内存泄漏检测功能,可以检测程序是否存在未正确释放的内存等。


三、出现什么问题
如果我们开发的应用程序,只有一个exe模块,那么,使用FastMM是一件非常简单的事情,只需要把FastMM.pas(最新版是FastMM4.pas)作为工程文件的第一个uses单元即可,如:

program Test;
uses
    FastMM4,
    …
但是,通常情况下,我们的应用程序都是由一个exe模块加上多个dll组成的,这样,当我们跨模块传递动态内存变量如string变量时,就会出问题,比如,下面的测试程序由一个exe和一个dll组成:

library test;   // test.dll
uses
    FastMM4, …;
procedure GetStr(var S: string; const Len: Integer); stdcall;
begin
    SetLength(S, Len); // 分配内存
    FillChar(S[1], Len, ‘A’);   
end;
exports
    GetStr;
-------------------------------------
program TestPrj;
uses
    FastMM4, …;
//------------------
unit mMain; // 测试界面

procedure TForm1.btnDoClick(Sender: TObject);
var
I: Integer;
S: string;
Begin
try 
for I := 1 to 10000 do
begin
    GetStr(S, I + 1);
    edt1.Text := S;
    Application.ProcessMessages;
end;
finally
    SetLength(S, 0);
end;
end;

当第二次执行btnDoClick过程时,就会出现内存错误,为什么这样?delphi的字符串是带引用计数的,跟接口变量一样,一旦这个引用计数为0,则会自动释放内存。在btnDoClick过程中,调用GetStr过程,用SetLength给S分配了一段内存,此时这个字符串的引用计数为1,然后执行edt1.Text := S语句,字符串的引用计数为2,循环再调用GetStr给S重新分配内存,这样原来的字符串的引用计数减1,再执行edt1.Text := S,原来的字符串引用计数为0,这时,就会被释放(注意,是在TestPrj.exe释放,而不是在Test.dll释放),但这时没有出错,当循环执行完毕之后,还有一个字符串的引用计数为2,但是执行SetLength(S, 0)之后,该字符串被edt1.Text引用,的引用计数为1,第二次执行btnDoClick时,执行edt1.Text := S时,上次的引用计数为1的字符串引用计数减一变为0,就会被释放,此时,会出现内存错误。
由此,可以看到,在另一个模块释放别的模块分配的内存,并不一定马上出现内存错误,但是,如果频繁执行,则会出现内存错误,这种不确定的错误带有很大的隐蔽性,常常在调试时不出现,但实际应用时出现,不仔细分析很难找到原因。
要解决这个问题,就要从根源找起,这个根源就是内存管理。
Delphi的内存管理,Delphi应用程序可以使用的有三种内存区:全局内存区、堆、栈,全局内存区存储全局变量、栈用来传递参数以及返回值,以及函数内的临时变量,这两种都是由编译器自动管理,而如字符串、对象、动态数组等都是从堆中分配的,内存管理就是指对堆内存管理,即从堆中分配内存和释放从堆中分配的内存(以下称内存的分配和释放)。
我们知道,一个进程只有一个栈,因此,也很容易误以为一个进程也只有一个堆,但实际上,一个进程除了拥有一个系统分配的默认堆(默认大小1MB),还可以创建多个用户堆,每个堆都有自己的句柄,delphi的内存管理所管理的正是自行创建的堆,delphi还把一个堆以链表的形式分成多个大小不等的块,实际的内存操作都是在这些块上。
delphi把内存管理定义为内存的分配(Get)、释放(Free)和重新分配(Realloc)。内存管理器也就是这三种实现的一个组合,delphi在system单元中定义了这个内存管理器TMemoryManager:

PMemoryManager = ^TMemoryManager;
TMemoryManager = record
    GetMem: function (Size: Integer): Pointer;
    FreeMem: function (P: Pointer): Integer;
    ReallocMem: function (P: Pointer; Size: Integer): Pointer;
end;
由此知道,delphi的内存管理器就是一个 TMemoryManager 记录对象,该记录有三个域,分别指向内存的分配、释放和重新分配例程。
System单元还定义了一个变量 MemoryManager :
MemoryManager: TMemoryManager = (
    GetMem: SysGetMem;
    FreeMem: SysFreeMem;
    ReallocMem: SysReallocMem);
该变量是delphi程序的内存管理器,缺省情况下,这个内存管理器的三个域分别指向GETMEM.INC中实现的SysGetMem、SysFreeMem、SysReallocMem。这个内存管理器变量只在system.pas中可见,但是system单元提供了三个可以访问该变量的例程:

// 读取内存管理器,也即读取MemoryManager
procedure GetMemoryManager (var MemMgr: TMemoryManager);
// 安装内存管理器(即用新的内存管理器替换缺省的内存管理器)
procedure SetMemoryManager (const MemMgr: TMemoryManager);
// 是否已经安装了内存管理器(即缺省的内存管理器是否已经被替换)
function IsMemoryManagerSet: Boolean;
四、共享内存管理器
什么是共享内存管理器?
所谓共享内存管理器,就是一个应用程序的所有的模块,不管是exe还是dll,都使用同一个内存管理器来管理内存,这样,内存的分配和释放都是同一个内存管理器完成的,就不会出现内存错误的问题。
那么如何共享内存管理器呢?
由上分析,我们可以知道,既然要使用同一个内存管理器,那么干脆就创建一个独立的内存管理器模块(dll),其他的所有模块都使用这个模块的内存管理器来分配和释放内存。Delphi7默认就是采取这种方法,当我们使用向导创建一个dll工程时,工程文件会有这样一段话:
{Important note about DLL memory management: ShareMem must be the
first unit in your library's USES clause AND your project's (select
Project-View Source) USES clause if your DLL exports any procedures or
functions that pass strings as parameters or function results. This
applies to all strings passed to and from your DLL--even those that
are nested in records and classes. ShareMem is the interface unit to
the BORLNDMM.DLL shared memory manager, which must be deployed along
with your DLL. To avoid using BORLNDMM.DLL, pass string information
using PChar or ShortString parameters. }
这段话提示我们,ShareMem 是 BORLNDMM.DLL 共享内存管理器的接口单元,我们来看看这个ShareMem,这个单元文件很简短,其中有这样的声明:

const
DelphiMM = 'borlndmm.dll';
function SysGetMem (Size: Integer): Pointer;
external DelphiMM name '@Borlndmm@SysGetMem$qqri';
function SysFreeMem(P: Pointer): Integer; 
external DelphiMM name '@Borlndmm@SysFreeMem$qqrpv';
function SysReallocMem(P: Pointer; Size: Integer): Pointer; 
external DelphiMM name '@Borlndmm@SysReallocMem$qqrpvi';
这些声明保证了BORLNDMM.DLL将被静态加载。
在ShareMem的Initialization是这样的代码:
if not IsMemoryManagerSet then
    InitMemoryManager;
首先判断内存管理器是否已经被安装(也即是否默认的内存管理器被替换掉),如果没有,则初始化内存管理器,InitMemoryManager也非常简单(把无用的代码去掉了):

procedure InitMemoryManager;
var
SharedMemoryManager: TMemoryManager;
MM: Integer;
begin
// force a static reference to borlndmm.dll, so we don't have to LoadLibrary
SharedMemoryManager.GetMem:= SysGetMem;
MM: = GetModuleHandle (DelphiMM);
SharedMemoryManager.GetMem:= GetProcAddress (MM,'@Borlndmm@SysGetMem$qqri');
SharedMemoryManager.FreeMem:= GetProcAddress (MM,'@Borlndmm@SysFreeMem$qqrpv');
SharedMemoryManager.ReallocMem:= GetProcAddress (MM, '@Borlndmm@SysReallocMem$qqrpvi');
SetMemoryManager (SharedMemoryManager);
end;
这个函数定义了一个内存管理器对象,并设置域指向Borlndmm.dll的三个函数实现,然后调用SetMemoryManager来替换默认的内存管理器。
这样,不管那个模块,因为都要将ShareMem作为工程的第一个uses单元,因此,每个模块的ShareMem的Initialization都是最先被执行的,也就是说,每个模块的内存管理器对象虽然不相同,但是,内存管理器的三个函数指针都是指向Borlndmm.dll的函数实现,因此,所有模块的内存分配和释放都是在Borlndmm.dll内部完成的,这样就不会出现跨模块释放内存导致错误的问题。
那么,FastMM又是如何实现共享内存管理器呢?
FastMM采取了一个原理上很简单的办法,就是创建一个内存管理器,然后将这个内存管理器的地址放到一个进程内所有模块都能读取到的位置,这样,其他模块在创建内存管理器之前,先查查是否有别的模块已经把内存管理器放到这个位置,如果是则使用这个内存管理器,否则才创建一个新的内存管理器,并将地址放到这个位置,这样,这个进程的所有模块都使用一个内存管理器,实现了内存管理器的共享。
而且,这个内存管理器并不确定是哪个模块创建的,所有的模块,只要将FastMM作为其工程文件的第一个uses单元,就有可能是这个内存管理器的创建者,关键是看其在应用程序的加载顺序,第一个被加载的模块将成为内存管理器的创建者。
那么,FastMM具体是如何实现的呢?
打开 FastMM4.pas(FastMM的最新版本),还是看看其Initialization部分:

{Initialize all the lookup tables, etc. for the memory manager}
InitializeMemoryManager;
{Has another MM been set, or has the Borland MM been used? If so, this file
   is not the first unit in the uses clause of the project's .dpr file.}
if CheckCanInstallMemoryManager then
begin
    InstallMemoryManager;
end;
InitializeMemoryManager 是初始化一些变量,完成之后就调用CheckCanInstallMemoryManager检测FastMM是否是作为工程的第一个uses单元,如果返回True,则调用InstallMemoryManager安装FastMM自己的内存管理器,我们按顺序摘取该函数的关键性代码进行分析:
{Build a string identifying the current process}
LCurrentProcessID: = GetCurrentProcessId;
for i := 0 to 7 do
UniqueProcessIDString [8 - i]:= HexTable [((LCurrentProcessID shr (i * 4)) and $F)];
MMWindow: = FindWindow ('STATIC', PChar (@UniqueProcessIDString [1]));
首先,获取该进程的ID,并转换为十六进制字符串,然后查找以该字符串为窗口名称的窗口。
如果进程中还没有该窗口,则MMWindow 将返回0,那就,就创建该窗口:
MMWindow: = CreateWindow ('STATIC', PChar (@UniqueProcessIDString [1]),
          WS_POPUP, 0, 0, 0, 0, 0, 0, hInstance, nil);
创建这个窗口有什么用呢,继续看下面的代码:

if MMWindow <> 0 then
SetWindowLong (MMWindow, GWL_USERDATA, Integer (@NewMemoryManager));
NewMemoryManager.Getmem: = FastGetMem;
NewMemoryManager.FreeMem: = FastFreeMem;
NewMemoryManager.ReallocMem: = FastReallocMem;
查阅MSDN可以知道,每个窗口都有一个可供创建它的应用程序使用的32位的值,该值可以通过以GWL_USERDATA为参数调用SetWindowLong来进行设置,也可以通过以GWL_USERDATA为参数调用GetWindowLong来读取。由此,我们就很清楚地知道,原来FastMM把要共享的内存管理器的地址保存到这个值上,这样其他模块就可以通过GetWindowLong来获读取到这个值,从而得到共享的内存管理器:

NewMemoryManager: = PMemoryManager (GetWindowLong (MMWindow, GWL_USERDATA)) ^;
通过上面的分析,可以看出,FastMM 在实现共享内存管理器上要比borland巧妙得多,borland的实现方法使得应用程序必须将BORLNDMM.DLL一起发布,而FastMM的实现方法不需要任何dll的支持。
但是,上面的摘录代码把一些编译选项判断忽略掉了,实际上,要使用FastMM的共享内存管理器功能,需要在各个模块编译的时候在FastMM4.pas单元上打开一些编译选项:
{$define ShareMM} //是打开共享内存管理器功能,是其他两个编译选项生效的前提
{$define ShareMMIfLibrary} //是允许一个dll共享其内存管理器,如果没有定义这个选项,则一个应用程序中,只有exe模块才能够创建和共享其内存管理器,由于静态加载的dll总是比exe更早被加载,因此,如果一个dll会被静态加载,则必须打开该选项,否则可能会出错
{$define AttemptToUseSharedMM} //是允许一个模块使用别的模块共享的内存管理器
这些编译选项在FastMM4.pas所在目录的FastMM4Options.inc文件中都有定义和说明,只不过这些定义都被注释掉了,因此,可以取消注释来打开这些编译选项,或者可以在你的工程目录下创建一个.inc文件(如FastShareMM.inc),把这些编译选项写入这个文件中,然后,在FastMM4.pas开头的“{$Include FastMM4Options.inc}”之前加入“{$include FastShareMM.inc}”即可,这样,不同的工程可以使用不同的FastShareMM.inc文件。
五、多线程下的内存管理
多线程环境下,内存管理是安全的吗?显然,如果不采取一定的措施,那么肯定是不安全的,borland已经考虑到这种情况,因此,在delphi的system.pas中定义了一个系统变量IsMultiThread,这个系统变量指示当前是否为多线程环境,那么,它是如何工作的?打开TThread.Create函数的代码可以看到它调用了BeginThread来创建一个线程,而BeginThread把IsMultiThread设置为了True.
再来看看GETMEM.INC的SysGetMem、SysFreeMem、SysReallocMem的实现,可以看到,在开始都由这样的语句:
if IsMultiThread then EnterCriticalSection(heapLock);
也就是说,在多线程环境下,内存的分配和释放都要用临界区进行同步以保证安全。
而FastMM则使用了一条CUP指令lock来实现同步,该指令作为其他指令的前缀,可以在在一条指令的执行过程中将总线锁住,当然,也是在IsMultiThread为True的情况下才会进行同步。
而IsMultiThread是定义在system.pas的系统变量,每个模块(exe或者dll)都有自己的IsMultiThread变量,并且,默认为Fasle,只有该模块中创建了用户线程,才会把这个变量设置为True,因此,我们在exe中创建线程,只会把exe中的IsMultiThread设置为True,并不会把其他的dll模块中的IsMultiThread设置为True,但是,前面已经说过,如果我们使用了静态加载的dll,这些dll将会比exe更早被系统加载,这时,第一个被加载的dll就会创建一个内存管理器并共享出来,其他模块都会使用这个内存管理器,也就是说,exe的IsMultiThread变量没有影响到应用程序的内存管理器,内存管理器还是认为当前不是多线程环境,因此,没有进行同步,这样就会出现内存错误的情况。
解决这个问题就是要想办法当处于多线程环境时,让所有的模块的IsMultiThread都设置为True,以保证不管哪个模块实际创建了内存管理器,该内存管理器都知道当前是多线程环境,需要进行同步处理。
还好,windows提供了一个机制,可以让我们的dll知道应用程序创建了线程。DllMain函数是dll动态链接库的入口函数,delphi把这个入口函数封装起来,提供了一个TDllProc的函数类型和一个该类型的变量DllProc:

TDLLProc = procedure (Reason: Integer); // 定义在system.pas
// 定义在sysinit.pas:
var 
    DllProc: TDllProc;

当系统调用dll的DllMain时,delphi最后会调用DllProc进行处理,DllProc可以被指向我们自己的TDllProc实现。而当进程创建了一个新线程时,操作系统会以Reason=DLL_THREAD_ATTACH为参数调用DllMain,那么delphi最后会以该参数调用DllProc,因此我们只要实现一个新的TDllProc实现ThisDllProc,并将DllProc指向ThisDllProc,而在ThisDllProc中,当收到DLL_THREAD_ATTACH时把IsMultiThread设置为True即可。实现代码如下:

library xxx;
var
OldDllProc: TDLLProc;
procedure ThisDllProc(Reason: Integer);
begin
if Reason = DLL_THREAD_ATTACH then
    IsMultiThread := True;
if Assigned(OldDllProc) then
    OldDllProc(Reason);
end;
begin
OldDllProc := DllProc;
DllProc := ThisDllProc;
ThisDllProc(DLL_PROCESS_ATTACH);
六、总结
本文主要讨论了下面几个问题:
    1、为什么要使用FastMM
    2、跨模块传递动态内存变量会出现什么问题,原因是什么
    3、delphi的内存管理和内存管理器是怎么回事
     4、为什么要共享内存管理器,delphi和FastMM分别是如何实现内存管理器共享的
     5、多线程环境下,内存管理器如何实现同步
     6、多线程环境下,如何跨模块设置IsMultiThread变量以保证内存管理器会进行同步

要正确使用FastMM,在模块开发的时候需要完成以下工作:
    1、打开编译选项{$define ShareMM}、{$define ShareMMIfLibrary}、{$define AttemptToUseSharedMM}
    2、将FastMM(4).pas作为每个工程文件的第一个uses单元 
    3、如果是dll,需要处理以DLL_THREAD_ATTACH为参数的DllMain调用,设置IsMultiThread为True

七、参考文献
《Windows 程序设计第五版》[美]Charles Petzold著,北京大学出版社
《Delphi源代码分析》 周爱民 著,电子工业出版社

FastMM使用详解的更多相关文章

  1. [转] FastMM使用详解

    FastMM使用详解 一.引言      FastMM 是适用于delphi的第三方内存管理器,在国外已经是大名鼎鼎,在国内也有许多人在使用或者希望使用,就连 Borland 也在delphi2007 ...

  2. FastMM配置文件详解

    FastMM最新版本提供了中文语言包,可方便国内使用.下载地址为 http://sourceforge.net/projects/fastmm/ 配置文件为:FastMM4Options.inc, 中 ...

  3. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  4. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  5. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  6. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  7. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  8. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  9. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...

随机推荐

  1. shell脚本,文件里面的英文大小写替换方法。

    [root@localhost wyb]# cat daxiaoxie qweBNMacb eeeDFSmkl svdIOPtyu [root@localhost wyb]# cat daxiaoxi ...

  2. WinPcap过滤串表达式的语法

    注意:这篇文档取自tcpdump的指南.原始的版本 www.tcpdump.org 找到.   wpcap的过滤器是以已声明的谓词语法为基础的.过滤器是一个ASCII字符串,它包含了一个过滤表达式.p ...

  3. Greenplum介绍-table

    GP中的table和其它关系型数据表是一样的,除了数据被分布在不同的segment以外. 建表时需定义以下几个方面:1. 指定列和数据类型2. 约束3. 分布策略4. 数据存储方式5. 大表分区策略 ...

  4. svn设置提交时忽略某些文件

    一.在资源管理器中,右键一个未加入版本控制文件或目录,并从弹出菜单选择TortoiseSVN →Add to Ignore List,会出现一个子菜单,允许你仅选择该文件或者所有具有相同后缀的文件. ...

  5. dom事件机制系列

    JS事件流机制 一个完整的JS事件流是从window开始,最后回到window的一个过程,事件流被分为三个阶段: (1~5)捕获过程.(5~6)目标过程.(6~10)冒泡过程. 通过addEventL ...

  6. 【ios】IOS返回3824错误

    后台接口数据返回有NULL格式的数据

  7. 如何删除SQL Server 2014连接到服务器中的服务器名称

    查看了网上的一些方法,均是以前版本的处理方法,不过原理都是删除"SqlStudio.bin",通过“Everything”搜索SqlStudio.bin,位置是: C:\Users ...

  8. Python中如何将数据存储为json格式的文件

    一.基于json模块的存储.读取数据 names_writer.py import json names = ['joker','joe','nacy','timi'] filename='names ...

  9. IPy模块--IP地址处理

    Python之实用的IP地址处理模块IPy 实用的IP地址处理模块IPy 在IP地址规划中,涉及到计算大量的IP地址,包括网段.网络掩码.广播地址.子网数.IP类型等 别担心,Ipy模块拯救你.Ipy ...

  10. HDU 3341 Lost's revenge

    Lost's revenge Time Limit: 5000ms Memory Limit: 65535KB This problem will be judged on HDU. Original ...