1. 程式人生 > >[轉] Delphi7 記憶體管理及 FastMM 研究

[轉] Delphi7 記憶體管理及 FastMM 研究

Delphi7 記憶體管理及 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 '@

[email protected]$qqri';
function SysFreeMem(P: Pointer): Integer; 
external DelphiMM name '@[email protected]$qqrpv';
function SysReallocMem(P: Pointer; Size: Integer): Pointer; 
external DelphiMM name '@[email protected]$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,'@
[email protected]
$qqri');
SharedMemoryManager.FreeMem:= GetProcAddress (MM,'@[email protected]$qqrpv');
SharedMemoryManager.ReallocMem:= GetProcAddress (MM, '@[email protected]$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原始碼分析》 周愛民 著,電子工業出版社