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,F(xiàn)astMM4的性能提高達88%.
第二個原因FastMM的共享內存管理器功能使用簡單可靠。當一個應用程序有多個模塊(exe和dll)組成時,模塊之間的動態(tài)內存變量如string的傳遞就是一個很大的問題,缺省情況下,各個模塊都由自己的內存管理器,由一個內存管理器分配的內存也必須在這個內存管理器才能安全釋放,否則就會出現(xiàn)內存錯誤,這樣如果在一個模塊分配的內存在另外一個模塊釋放就會出現(xiàn)內存錯誤。解決這個問題就需要使用到共享內存管理器,讓各個模塊都使用同一個內存管理器。Delphi缺省的共享內存管理器是BORLNDMM.DLL,這個內存管理器并不可靠,也常常出現(xiàn)問題,并且,在程序發(fā)布的時候必須連同這個DLL一起發(fā)布。而FastMM的共享內存管理器功能不需要DLL支持,并且更加可靠。
第三個原因是FastMM還擁有一些幫助程序開發(fā)的輔助功能,如內存泄漏檢測功能,可以檢測程序是否存在未正確釋放的內存等。
三、出現(xiàn)什么問題
如果我們開發(fā)的應用程序,只有一個exe模塊,那么,使用FastMM是一件非常簡單的事情,只需要把FastMM.pas(最新版是FastMM4.pas)作為工程文件的第一個uses單元即可,如:
program Test;
uses
FastMM4,
…
但是,通常情況下,我們的應用程序都是由一個exe模塊加上多個dll組成的,這樣,當我們跨模塊傳遞動態(tài)內存變量如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;
當?shù)诙螆?zhí)行btnDoClick過程時,就會出現(xiàn)內存錯誤,為什么這樣?delphi的字符串是帶引用計數(shù)的,跟接口變量一樣,一旦這個引用計數(shù)為0,則會自動釋放內存。在btnDoClick過程中,調用GetStr過程,用SetLength給S分配了一段內存,此時這個字符串的引用計數(shù)為1,然后執(zhí)行edt1.Text := S語句,字符串的引用計數(shù)為2,循環(huán)再調用GetStr給S重新分配內存,這樣原來的字符串的引用計數(shù)減1,再執(zhí)行edt1.Text := S,原來的字符串引用計數(shù)為0,這時,就會被釋放(注意,是在TestPrj.exe釋放,而不是在Test.dll釋放),但這時沒有出錯,當循環(huán)執(zhí)行完畢之后,還有一個字符串的引用計數(shù)為2,但是執(zhí)行SetLength(S, 0)之后,該字符串被edt1.Text引用,的引用計數(shù)為1,第二次執(zhí)行btnDoClick時,執(zhí)行edt1.Text := S時,上次的引用計數(shù)為1的字符串引用計數(shù)減一變?yōu)?,就會被釋放,此時,會出現(xiàn)內存錯誤。
由此,可以看到,在另一個模塊釋放別的模塊分配的內存,并不一定馬上出現(xiàn)內存錯誤,但是,如果頻繁執(zhí)行,則會出現(xiàn)內存錯誤,這種不確定的錯誤帶有很大的隱蔽性,常常在調試時不出現(xiàn),但實際應用時出現(xiàn),不仔細分析很難找到原因。
要解決這個問題,就要從根源找起,這個根源就是內存管理。
一、Delphi的內存管理
Delphi應用程序可以使用的有三種內存區(qū):全局內存區(qū)、堆、棧,全局內存區(qū)存儲全局變量、棧用來傳遞參數(shù)以及返回值,以及函數(shù)內的臨時變量,這兩種都是由編譯器自動管理,而如字符串、對象、動態(tài)數(shù)組等都是從堆中分配的,內存管理就是指對堆內存管理,即從堆中分配內存和釋放從堆中分配的內存(以下稱內存的分配和釋放)。
我們知道,一個進程只有一個棧,因此,也很容易誤以為一個進程也只有一個堆,但實際上,一個進程除了擁有一個系統(tǒng)分配的默認堆(默認大小1MB),還可以創(chuàng)建多個用戶堆,每個堆都有自己的句柄,delphi的內存管理所管理的正是自行創(chuàng)建的堆,delphi還把一個堆以鏈表的形式分成多個大小不等的塊,實際的內存操作都是在這些塊上。
delphi把內存管理定義為內存的分配(Get)、釋放(Free)和重新分配(Realloc)。內存管理器也就是這三種實現(xiàn)的一個組合,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中實現(xiàn)的SysGetMem、SysFreeMem、SysReallocMem。這個內存管理器變量只在system.pas中可見,但是system單元提供了三個可以訪問該變量的例程:
// 讀取內存管理器,也即讀取MemoryManager
procedure GetMemoryManager (var MemMgr: TMemoryManager);
// 安裝內存管理器(即用新的內存管理器替換缺省的內存管理器)
procedure SetMemoryManager (const MemMgr: TMemoryManager);
// 是否已經安裝了內存管理器(即缺省的內存管理器是否已經被替換)
function IsMemoryManagerSet: Boolean;
四、共享內存管理器
什么是共享內存管理器?
所謂共享內存管理器,就是一個應用程序的所有的模塊,不管是exe還是dll,都使用同一個內存管理器來管理內存,這樣,內存的分配和釋放都是同一個內存管理器完成的,就不會出現(xiàn)內存錯誤的問題。
那么如何共享內存管理器呢?
由上分析,我們可以知道,既然要使用同一個內存管理器,那么干脆就創(chuàng)建一個獨立的內存管理器模塊(dll),其他的所有模塊都使用這個模塊的內存管理器來分配和釋放內存。Delphi7默認就是采取這種方法,當我們使用向導創(chuàng)建一個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將被靜態(tài)加載。
在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;
這個函數(shù)定義了一個內存管理器對象,并設置域指向Borlndmm.dll的三個函數(shù)實現(xiàn),然后調用SetMemoryManager來替換默認的內存管理器。
這樣,不管那個模塊,因為都要將ShareMem作為工程的第一個uses單元,因此,每個模塊的ShareMem的Initialization都是最先被執(zhí)行的,也就是說,每個模塊的內存管理器對象雖然不相同,但是,內存管理器的三個函數(shù)指針都是指向Borlndmm.dll的函數(shù)實現(xiàn),因此,所有模塊的內存分配和釋放都是在Borlndmm.dll內部完成的,這樣就不會出現(xiàn)跨模塊釋放內存導致錯誤的問題。
那么,F(xiàn)astMM又是如何實現(xiàn)共享內存管理器呢?
FastMM采取了一個原理上很簡單的辦法,就是創(chuàng)建一個內存管理器,然后將這個內存管理器的地址放到一個進程內所有模塊都能讀取到的位置,這樣,其他模塊在創(chuàng)建內存管理器之前,先查查是否有別的模塊已經把內存管理器放到這個位置,如果是則使用這個內存管理器,否則才創(chuàng)建一個新的內存管理器,并將地址放到這個位置,這樣,這個進程的所有模塊都使用一個內存管理器,實現(xiàn)了內存管理器的共享。
而且,這個內存管理器并不確定是哪個模塊創(chuàng)建的,所有的模塊,只要將FastMM作為其工程文件的第一個uses單元,就有可能是這個內存管理器的創(chuàng)建者,關鍵是看其在應用程序的加載順序,第一個被加載的模塊將成為內存管理器的創(chuàng)建者。
那么,F(xiàn)astMM具體是如何實現(xiàn)的呢?
打開 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自己的內存管理器,我們按順序摘取該函數(shù)的關鍵性代碼進行分析:
{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,那就,就創(chuàng)建該窗口:
MMWindow: = CreateWindow ('STATIC', PChar (@UniqueProcessIDString [1]),
WS_POPUP, 0, 0, 0, 0, 0, 0, hInstance, nil);
創(chuàng)建這個窗口有什么用呢,繼續(xù)看下面的代碼:
if MMWindow <> 0 then
SetWindowLong (MMWindow, GWL_USERDATA, Integer (@NewMemoryManager));
NewMemoryManager.Getmem: = FastGetMem;
NewMemoryManager.FreeMem: = FastFreeMem;
NewMemoryManager.ReallocMem: = FastReallocMem;
查閱MSDN可以知道,每個窗口都有一個可供創(chuàng)建它的應用程序使用的32位的值,該值可以通過以GWL_USERDATA為參數(shù)調用SetWindowLong來進行設置,也可以通過以GWL_USERDATA為參數(shù)調用GetWindowLong來讀取。由此,我們就很清楚地知道,原來FastMM把要共享的內存管理器的地址保存到這個值上,這樣其他模塊就可以通過GetWindowLong來獲讀取到這個值,從而得到共享的內存管理器:
NewMemoryManager: = PMemoryManager (GetWindowLong (MMWindow, GWL_USERDATA)) ^;
通過上面的分析,可以看出,F(xiàn)astMM 在實現(xiàn)共享內存管理器上要比borland巧妙得多,borland的實現(xiàn)方法使得應用程序必須將BORLNDMM.DLL一起發(fā)布,而FastMM的實現(xiàn)方法不需要任何dll的支持。
但是,上面的摘錄代碼把一些編譯選項判斷忽略掉了,實際上,要使用FastMM的共享內存管理器功能,需要在各個模塊編譯的時候在FastMM4.pas單元上打開一些編譯選項:
{$define ShareMM} //是打開共享內存管理器功能,是其他兩個編譯選項生效的前提
{$define ShareMMIfLibrary} //是允許一個dll共享其內存管理器,如果沒有定義這個選項,則一個應用程序中,只有exe模塊才能夠創(chuàng)建和共享其內存管理器,由于靜態(tài)加載的dll總是比exe更早被加載,因此,如果一個dll會被靜態(tài)加載,則必須打開該選項,否則可能會出錯
{$define AttemptToUseSharedMM} //是允許一個模塊使用別的模塊共享的內存管理器
這些編譯選項在FastMM4.pas所在目錄的FastMM4Options.inc文件中都有定義和說明,只不過這些定義都被注釋掉了,因此,可以取消注釋來打開這些編譯選項,或者可以在你的工程目錄下創(chuàng)建一個.inc文件(如FastShareMM.inc),把這些編譯選項寫入這個文件中,然后,在FastMM4.pas開頭的“{$Include FastMM4Options.inc}”之前加入“{$include FastShareMM.inc}”即可,這樣,不同的工程可以使用不同的FastShareMM.inc文件。
五、多線程下的內存管理
多線程環(huán)境下,內存管理是安全的嗎?顯然,如果不采取一定的措施,那么肯定是不安全的,borland已經考慮到這種情況,因此,在delphi的system.pas中定義了一個系統(tǒng)變量IsMultiThread,這個系統(tǒng)變量指示當前是否為多線程環(huán)境,那么,它是如何工作的?打開TThread.Create函數(shù)的代碼可以看到它調用了BeginThread來創(chuàng)建一個線程,而BeginThread把IsMultiThread設置為了True.
再來看看GETMEM.INC的SysGetMem、SysFreeMem、SysReallocMem的實現(xiàn),可以看到,在開始都由這樣的語句:
if IsMultiThread then EnterCriticalSection(heapLock);
也就是說,在多線程環(huán)境下,內存的分配和釋放都要用臨界區(qū)進行同步以保證安全。
而FastMM則使用了一條CUP指令lock來實現(xiàn)同步,該指令作為其他指令的前綴,可以在在一條指令的執(zhí)行過程中將總線鎖住,當然,也是在IsMultiThread為True的情況下才會進行同步。
而IsMultiThread是定義在system.pas的系統(tǒng)變量,每個模塊(exe或者dll)都有自己的IsMultiThread變量,并且,默認為Fasle,只有該模塊中創(chuàng)建了用戶線程,才會把這個變量設置為True,因此,我們在exe中創(chuàng)建線程,只會把exe中的IsMultiThread設置為True,并不會把其他的dll模塊中的IsMultiThread設置為True,但是,前面已經說過,如果我們使用了靜態(tài)加載的dll,這些dll將會比exe更早被系統(tǒng)加載,這時,第一個被加載的dll就會創(chuàng)建一個內存管理器并共享出來,其他模塊都會使用這個內存管理器,也就是說,exe的IsMultiThread變量沒有影響到應用程序的內存管理器,內存管理器還是認為當前不是多線程環(huán)境,因此,沒有進行同步,這樣就會出現(xiàn)內存錯誤的情況。
解決這個問題就是要想辦法當處于多線程環(huán)境時,讓所有的模塊的IsMultiThread都設置為True,以保證不管哪個模塊實際創(chuàng)建了內存管理器,該內存管理器都知道當前是多線程環(huán)境,需要進行同步處理。
還好,windows提供了一個機制,可以讓我們的dll知道應用程序創(chuàng)建了線程。DllMain函數(shù)是dll動態(tài)鏈接庫的入口函數(shù),delphi把這個入口函數(shù)封裝起來,提供了一個TDllProc的函數(shù)類型和一個該類型的變量DllProc:
TDLLProc = procedure (Reason: Integer); // 定義在system.pas
// 定義在sysinit.pas:
var
DllProc: TDllProc;
當系統(tǒng)調用dll的DllMain時,delphi最后會調用DllProc進行處理,DllProc可以被指向我們自己的TDllProc實現(xiàn)。而當進程創(chuàng)建了一個新線程時,操作系統(tǒng)會以Reason=DLL_THREAD_ATTACH為參數(shù)調用DllMain,那么delphi最后會以該參數(shù)調用DllProc,因此我們只要實現(xiàn)一個新的TDllProc實現(xiàn)ThisDllProc,并將DllProc指向ThisDllProc,而在ThisDllProc中,當收到DLL_THREAD_ATTACH時把IsMultiThread設置為True即可。實現(xiàn)代碼如下:
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、跨模塊傳遞動態(tài)內存變量會出現(xiàn)什么問題,原因是什么
3、delphi的內存管理和內存管理器是怎么回事
4、為什么要共享內存管理器,delphi和FastMM分別是如何實現(xiàn)內存管理器共享的
5、多線程環(huán)境下,內存管理器如何實現(xiàn)同步
6、多線程環(huán)境下,如何跨模塊設置IsMultiThread變量以保證內存管理器會進行同步
要正確使用FastMM,在模塊開發(fā)的時候需要完成以下工作:
1、打開編譯選項{$define ShareMM}、{$define ShareMMIfLibrary}、{$define AttemptToUseSharedMM}
2、將FastMM(4).pas作為每個工程文件的第一個uses單元
3、如果是dll,需要處理以DLL_THREAD_ATTACH為參數(shù)的DllMain調用,設置IsMultiThread為True
1> FastMM是開源項目, 從 http://sourceforge.net/projects/fastmm 下載最新版
2> 文件夾Replacement BorlndMM DLL/Precompiled/for Delphi IDE/Performance/BorlndMM.dll,替換掉Delphi/Bin下的相應文件就可以完成對IDE的提速
3> Enviroment->Library->Directories-> Library Path 添加FassMM路徑,我放在Delphi安裝目錄下,直接設置為$(DELPHI)/FastMM
4> 在你的項目文件中,Project->View Source打開后,uses 后第一個添加FastMM4單元
5> 編譯運行你的程序,如果有Memory leak,在關閉程序時會有一個提示對話框.
對話框也是可以關閉的
打開FastMM4Options.inc文件。在文件的末尾添加如下代碼:
{$define Release}
{$ifdef Release}
{Specify the options you use for release versions below}
{$undef FullDebugMode}
{$undef CheckHeapForCorruption}
{$define ASMVersion}
{$undef EnableMemoryLeakReporting}
{$undef UseOutputDebugString}
{$undef LogErrorsToFile}
{$undef LogMemoryLeakDetailToFile}
{$else}
{Specify the options you use for debugging below}
{$define FullDebugMode}
{$define EnableMemoryLeakReporting}
{$define UseOutputDebugString}{$endif}
重新打開把上面第一行條件編譯定義選項{$define Release} 注釋掉就可以了
如 //{$define Release} 即可.