Delphi7 內存管理及 FastMM 研究
作者:劉國輝
一、 引言
FastMM 是適用於delphi的第三方內存管理器,在國外已經是大名鼎鼎,在國內也有許多人在使用或者希望使用,就連 Borland 也在delphi2007拋棄了自己原有的飽受指責的內存管理器,改用FastMM.
但是,內存管理的復雜性以及缺乏 FastMM 中文文檔導致國內許多人在使用時遇到了許多問題,一些人因此而放棄了使用,我在最近的一個項目中使用了FastMM,也因此遇到了許多問題,經過摸索和研究,終於解決了這些問題。
二、 為什麼要用FastMM
第一個原因是FastMM的性能接近與delphi缺省內存管理器的兩倍,可以做一個簡單的測試,運行下面的代碼:
[delphi] view plaincopy
在我的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單元即可,如:
[delphi] view plaincopy
但是,通常情況下,我們的應用程序都是由一個exe模塊加上多個dll組成的,這樣,當我們跨模塊傳遞動態內存變量如string變量時,就會出問題,比如,下面的測試程序由一個exe和一個dll組成:
[delphi] view plaincopy
當第二次執行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:
[delphi] view plaincopy
由此知道,delphi的內存管理器就是一個 TMemoryManager 記錄對象,該記錄有三個域,分別指向內存的分配、釋放和重新分配例程。
System單元還定義了一個變量 MemoryManager :
[delphi] view plaincopy
該變量是delphi程序的內存管理器,缺省情況下,這個內存管理器的三個域分別指向GETMEM.INC中實現的SysGetMem、SysFreeMem、SysReallocMem。這個內存管理器變量只在system.pas中可見,但是system單元提供了三個可以訪問該變量的例程:
[delphi] view plaincopy
三、 共享內存管理器
什麼是共享內存管理器?
所謂共享內存管理器,就是一個應用程序的所有的模塊,不管是exe還是dll,都使用同一個內存管理器來管理內存,這樣,內存的分配和釋放都是同一個內存管理器完成的,就不會出現內存錯誤的問題。
那麼如何共享內存管理器呢?
由上分析,我們可以知道,既然要使用同一個內存管理器,那麼干脆就創建一個獨立的內存管理器模塊(dll),其他的所有模塊都使用這個模塊的內存管理器來分配和釋放內存。Delphi7默認就是采取這種方法,當我們使用向導創建一個dll工程時,工程文件會有這樣一段話:
[delphi] view plaincopy
這段話提示我們,ShareMem 是 BORLNDMM.DLL 共享內存管理器的接口單元,我們來看看這個ShareMem,這個單元文件很簡短,其中有這樣的聲明:
這些聲明保證了BORLNDMM.DLL將被靜態加載。
在ShareMem的Initialization是這樣的代碼:
首先判斷內存管理器是否已經被安裝(也即是否默認的內存管理器被替換掉),如果沒有,則初始化內存管理器,InitMemoryManager也非常簡單(把無用的代碼去掉了):
這個函數定義了一個內存管理器對象,並設置域指向Borlndmm.dll的三個函數實現,然後調用SetMemoryManager來替換默認的內存管理器。
這樣,不管那個模塊,因為都要將ShareMem作為工程的第一個uses單元,因此,每個模塊的ShareMem的Initialization都是最先被執行的,也就是說,每個模塊的內存管理器對象雖然不相同,但是,內存管理器的三個函數指針都是指向Borlndmm.dll的函數實現,因此,所有模塊的內存分配和釋放都是在Borlndmm.dll內部完成的,這樣就不會出現跨模塊釋放內存導致錯誤的問題。
那麼,FastMM又是如何實現共享內存管理器呢?
FastMM采取了一個原理上很簡單的辦法,就是創建一個內存管理器,然後將這個內存管理器的地址放到一個進程內所有模塊都能讀取到的位置,這樣,其他模塊在創建內存管理器之前,先查查是否有別的模塊已經把內存管理器放到這個位置,如果是則使用這個內存管理器,否則才創建一個新的內存管理器,並將地址放到這個位置,這樣,這個進程的所有模塊都使用一個內存管理器,實現了內存管理器的共享。
而且,這個內存管理器並不確定是哪個模塊創建的,所有的模塊,只要將FastMM作為其工程文件的第一個uses單元,就有可能是這個內存管理器的創建者,關鍵是看其在應用程序的加載順序,第一個被加載的模塊將成為內存管理器的創建者。
那麼,FastMM具體是如何實現的呢?
打開 FastMM4.pas(FastMM的最新版本),還是看看其Initialization部分:
InitializeMemoryManager 是初始化一些變量,完成之後就調用CheckCanInstallMemoryManager檢測FastMM是否是作為工程的第一個uses單元,如果返回True,則調用InstallMemoryManager安裝FastMM自己的內存管理器,我們按順序摘取該函數的關鍵性代碼進行分析:
首先,獲取該進程的ID,並轉換為十六進制字符串,然後查找以該字符串為窗口名稱的窗口。
如果進程中還沒有該窗口,則MMWindow 將返回0,那就,就創建該窗口:
創建這個窗口有什麼用呢,繼續看下面的代碼:
查閱MSDN可以知道,每個窗口都有一個可供創建它的應用程序使用的32位的值,該值可以通過以GWL_USERDATA為參數調用SetWindowLong來進行設置,也可以通過以GWL_USERDATA為參數調用GetWindowLong來讀取。由此,我們就很清楚地知道,原來FastMM把要共享的內存管理器的地址保存到這個值上,這樣其他模塊就可以通過GetWindowLong來獲讀取到這個值,從而得到共享的內存管理器:
通過上面的分析,可以看出,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:
當系統調用dll的DllMain時,delphi最後會調用DllProc進行處理,DllProc可以被指向我們自己的TDllProc實現。而當進程創建了一個新線程時,操作系統會以Reason=DLL_THREAD_ATTACH為參數調用DllMain,那麼delphi最後會以該參數調用DllProc,因此我們只要實現一個新的TDllProc實現ThisDllProc,並將DllProc指向ThisDllProc,而在ThisDllProc中,當收到DLL_THREAD_ATTACH時把IsMultiThread設置為True即可。實現代碼如下:
一、 總結
本文主要討論了下面幾個問題:
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源代碼分析》 周愛民 著,電子工業出版社
原文:http://blog.csdn.net/niniu/article/details/3404110