很長一段時間我都注意到我的服務器應用程序的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證實:
使用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的多線程服務器應用程序。
所以:
- 你覺得我的結論是什麼?
- 您是否有解決此問題的解決方法?
測試源代碼和二進制文件can be downloaded here。
感謝您的貢獻!
編輯:QC Report 105559。我等待着您的投票:-)
「做任何你有這個問題的解決方法:」我會使用32位應用程序,直到下一個穩定的 與64位編譯器的delphi的發佈隨之而來...... –
ComputerSaysNo
如果我是你,我會把它縮減爲最小尺寸的樣本,顯示泄漏,並將其提交給QC。 –
@DorinDuminica:這將是德爾福XE4然後;) – whosrdaddy