很长一段时间我都注意到我的服务器应用程序的Win64版本泄漏内存。虽然Win32版本在相对稳定的内存占用情况下运行良好,但64位版本使用的内存会定期增加 - 可能是20Mb /天,没有任何明显的原因(不用说,FastMM4没有报告任何内存泄漏) 。 32位和64位版本的源代码相同。该应用程序是围绕Indy TIdTCPServer组件构建的,它是一个连接到数据库的高度多线程服务器,该数据库处理由Delphi XE2制作的其他客户端发送的命令。
我花了很多时间查看自己的代码并试图理解为什么64位版本泄露了如此多的内存。最后我使用了专门用于跟踪DebugDiag和XPerf等内存泄漏的MS工具,看起来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显示泄漏的屏幕截图(查看名为“Heap”的红线)。以下屏幕截图是在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
Remy Lebeau helped me on the Embarcadero forums了解发生了什么:
第二次泄漏看起来更像是一个明确的错误。线程期间 shutdown,正在调用StartLib(),它调用ExitThreadTLS() 释放调用线程的TLS内存块,然后调用Halt0() 调用ExitDll()来引发一个被捕获的异常 DelphiExceptionHandler()调用AllocateRaiseFrame(),其中 在访问时间接调用GetTls()和InitThreadTLS() threadvar变量名为ExceptionObjectCount。重新分配 仍处于进程中的调用线程的TLS内存块 被关闭所以要么StartLib()不应该调用 在DLL_THREAD_DETACH期间Halt0()或DelphiExceptionHandler应该 检测到时不要调用AllocateRaiseFrame() _TExitDllException被引发。
对我来说,似乎很清楚Win64方式存在一个处理线程关闭的重大缺陷。此类行为禁止开发必须在Win64下运行27/7的任何多线程服务器应用程序。
所以:
测试源代码和二进制文件can be downloaded here。
感谢您的贡献!
修改:QC Report 105559。我在等你的选票: - )
答案 0 :(得分:2)
一个非常简单的方法是重新使用线程而不是创建和销毁它们。线程非常昂贵,你可能也会得到一个性能提升...虽然在调试方面感到荣幸......
答案 1 :(得分:0)
为了避免异常内存泄漏陷阱,您可以尝试在FoobarProc周围放置一个try / except。也许不是为了一个明确的解决方案,而是为了看看为什么首先提出了这个问题。
我通常有这样的事情:
try
FooBarProc()
except
if IsFatalException(ExceptObject) then // checks for system exceptions like AV, invalidop etc
OutputDebugstring(PChar(ExceptionToString(ExceptObject))) // or some other way of logging
end;
答案 2 :(得分:0)
我使用Delphi 10.2.3,至少在以下情况下,描述的问题似乎仍然存在。
// Remark: My TFooThread is created within the 64 Bit DLL:
procedure TFooThread.Execute;
begin
while not terminated do
try
ReadBlockingFromIndySocket();
ProcessData();
except on E:Exception do
begin
LogTheException(E.Message);
// Leave loop and thread
Abort;
end
end;
end;
每当剩下循环/线程时,这就会泄漏内存。 MadExcept泄漏报告显示,异常对象未销毁,在我的情况下,远程关闭连接时,大多数情况下为EIdConnClosedGracefully。发现问题是退出循环并因此退出线程的Abort语句。泄漏报告中的指示似乎可以证明@RemyLebeau的观察结果。在主程序而不是64位DLL中运行完全相同的代码不会泄漏任何内存。
解决方案:将Abort语句替换为Exit。
结论:64位DLL中的线程执行功能一定不能留下异常(Abort也是异常),否则该异常会导致内存泄漏。
至少这对我有用。