2012-05-11 36 views
35

很長一段時間我都注意到我的服務器應用程序的Win64版本泄漏了內存。雖然Win32版本在相對穩定的內存佔用情況下工作正常,但64位版本使用的內存有規律地增加 - 可能爲20Mb /天,沒有任何明顯的原因(不用說,FastMM4沒有報告任何內存泄漏) 。源代碼在32位和64位版本之間是相同的。該應用程序是圍繞Indy TIdTCPServer組件構建的,它是一個高度多線程的服務器,連接到一個數據庫,用於處理由Delphi XE2製作的其他客戶端發送的命令。線程關閉期間Win64 Delphi RTL中的內存泄漏?

我花了很多時間回顧我自己的代碼,並試圖瞭解爲什麼64位版本泄露這麼多的內存。我最終通過使用MS工具來追蹤DebugDiag和XPerf之類的內存泄漏,並且似乎在Delphi 64位RTL中存在一個基本缺陷,即每次線程脫離DLL時都會導致一些字節被泄漏。對於必須全天候運行而不重新啓動的高度多線程應用程序,此問題尤其嚴重。

我由一個主機應用程序和一個圖書館,都與XE2內置組成一個非常基本的項目複製的問題。該DLL與主機應用程序靜態鏈接。主機應用程序創建只需調用虛擬出口程序和退出線程:

這裏是庫的源代碼:

library FooBarDLL; 

uses 
    Windows, 
    System.SysUtils, 
    System.Classes; 

{$R *.res} 

function FooBarProc(): Boolean; stdcall; 
begin 
    Result := True; //Do nothing. 
end; 

exports 
    FooBarProc; 

主機應用程序使用定時器來創建一個線程,只是調用導出過程:

TFooThread = class (TThread) 
    protected 
    procedure Execute; override; 
    public 
    constructor Create; 
    end; 

... 

function FooBarProc(): Boolean; stdcall; external 'FooBarDll.dll'; 

implementation 

{$R *.dfm} 

procedure THostAppForm.TimerTimer(Sender: TObject); 
begin 
    with TFooThread.Create() do 
    Start; 
end; 

{ TFooThread } 

constructor TFooThread.Create; 
begin 
    inherited Create(True); 
    FreeOnTerminate := True; 
end; 

procedure TFooThread.Execute; 
begin 
    /// Call the exported procedure. 
    FooBarProc(); 
end; 

下面是一些截圖,顯示使用的VMMap泄漏(看看名爲「堆」紅線)。以下屏幕截圖是在30分鐘內完成的。

32位的二進制表示增加16個字節,這是完全可以接受的:

Memory usage for the 32 bit version http://img401.imageshack.us/img401/6159/soleak32.png

64位二進制表示12476個字節(從820K至13296K)的增加,這是更成問題:

Memory usage for the 64 bit version http://img12.imageshack.us/img12/209/soleak64.png

堆內存的不斷提高,它們也由XPerf證實:

XPerf usage http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

使用DebugDiag資料我能看到被分配泄漏的內存的代碼路徑:

LeakTrack+13529 
<my dll>!Sysinit::AllocTlsBuffer+13 
<my dll>!Sysinit::InitThreadTLS+2b 
<my dll>!Sysinit::::GetTls+22 
<my dll>!System::AllocateRaiseFrame+e 
<my dll>!System::DelphiExceptionHandler+342 
ntdll!RtlpExecuteHandlerForException+d 
ntdll!RtlDispatchException+45a 
ntdll!KiUserExceptionDispatch+2e 
KERNELBASE!RaiseException+39 
<my dll>!System::::RaiseAtExcept+106 
<my dll>!System::::RaiseExcept+1c 
<my dll>!System::ExitDll+3e 
<my dll>!System::::Halt0+54 
<my dll>!System::::StartLib+123 
<my dll>!Sysinit::::InitLib+92 
<my dll>!Smart::initialization+38 
ntdll!LdrShutdownThread+155 
ntdll!RtlExitUserThread+38 
<my application>!System::EndThread+20 
<my application>!System::Classes::ThreadProc+9a 
<my application>!SystemThreadWrapper+36 
kernel32!BaseThreadInitThunk+d 
ntdll!RtlUserThreadStart+1d 

雷米勒博helped me on the Embarcadero forums明白髮生了什麼事:

第二泄漏外觀更像是一個明確的錯誤。在線程 關閉期間,StartLib()被調用,它調用ExitThreadTLS()到 釋放調用線程的TLS內存塊,然後調用Halt0()到 調用ExitDll()來引發被 抓到的異常DelphiExceptionHandler調用AllocateRaiseFrame(),它 間接調用GetTls(),並因此InitThreadTLS()時,它訪問名爲ExceptionObjectCount一個 線程變量。這將重新分配正在關閉的仍在進程 中的調用線程的 TLS內存塊。因此,StartLib()不應該在DLL_THREAD_DETACH期間調用 Halt0(),或者DelphiExceptionHandler在檢測到 _TExitDllException異常時應該不會調用AllocateRaiseFrame()。

對我來說,看起來很清楚,Win64方式處理線程關閉存在一個主要缺陷。這種行爲禁止開發必須在Win64下運行27/7的多線程服務器應用程序。

所以:

  1. 你覺得我的結論是什麼?
  2. 您是否有解決此問題的解決方法?

測試源代碼和二進制文件can be downloaded here

感謝您的貢獻!

編輯QC Report 105559。我等待着您的投票:-)

+3

「做任何你有這個問題的解決方法:」我會使用32位應用程序,直到下一個穩定的與64位編譯器的delphi的發佈隨之而來...... – ComputerSaysNo

+4

如果我是你,我會把它縮減爲最小尺寸的樣本,顯示泄漏,並將其提交給QC。 –

+0

@DorinDuminica:這將是德爾福XE4然後;) – whosrdaddy

回答

2

一個非常簡單的解決辦法是重新使用線程,而不是創建和銷燬它們。線程是相當昂貴的,你可能會得到一個PERF升壓太...雖然上調試榮譽......

+0

是的,那是我第一個想法。在我的具體情況下,我當然可以使用線程池。但是,這不會阻止我的項目中包含的第三方代碼安排新線程,這些線程也會泄漏...... –

+0

這是真的,但如果您有第三方資源,則可以使用它來使用線程池,如果你不知道什麼,你可以做任何事情... 如果你絕對必須運行外部漏洞DLL,你應該在一個單獨的過程中做到這一點,你可以重新開始每一個時不時,但這並不總是可能的。 – Eric