我花了很多时间审查自己的代码,并试图了解为什么64位版本泄漏了这么多的内存。我最终通过使用MS工具设计来跟踪内存泄漏,如DebugDiag和XPerf,似乎有一个根本的缺陷在Delphi 64bit RTL,导致一些字节被泄漏每次一个线程已从DLL分离。这个问题对于必须每天24小时运行而不重新启动的高度多线程应用程序特别重要。
我复制了一个非常基本的项目,由主机应用程序和库组成的两个用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个字节,这是完全可以接受的:
64位二进制表示增加12476字节(从820K到13296K),这是更有问题:
堆内存的不断增加也由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
雷米Lebeau helped me on the Embarcadero forums了解发生了什么:
The second leak looks more like a definite bug. During thread
shutdown,StartLib() is being called,which calls ExitThreadTLS() to
free the calling thread’s TLS memory block,then calls Halt0() to
call ExitDll() to raise an exception that is caught by
DelphiExceptionHandler() to call AllocateRaiseFrame(),which
indirectly calls GetTls() and thus InitThreadTLS() when it accesses a
threadvar variable named ExceptionObjectCount. That re-allocates the
TLS memory block of the calling thread that is still in the process
of being shut down. So either StartLib() should not be calling
Halt0() during DLL_THREAD_DETACH,or DelphiExceptionHandler should
not be calling AllocateRaiseFrame() when it detects a
_TExitDllException being raised.
看起来很清楚,我有一个重大缺陷在Win64的方式来处理线程关闭。这种行为禁止开发任何必须在Win64下运行27/7的多线程服务器应用程序。
所以:
>你对我的结论有什么看法?
>你有任何一个解决这个问题的解决方法吗?
测试源代码和二进制代码can be downloaded here。
感谢您的贡献!
编辑:QC Report 105559.我在等你的投票:-)