线程关闭期间Win64 Delphi RTL内存泄漏?
很长一段时间以来,我注意到我的服务器应用程序的Win64版本泄漏内存。虽然Win32版本在相对稳定的内存占用下运行良好,但64位版本使用的内存会定期增加–可能是20Mb/天,没有任何明显的原因(不用说,FastMM4没有报告这两个版本的内存泄漏)。32位和64位版本的源代码相同。该应用程序是围绕Indy TIdTCPServer组件构建的,它是一个高度多线程的服务器,连接到一个数据库,该数据库处理由Delphi XE2生成的其他客户端发送的命令 我花了很多时间检查自己的代码,并试图理解为什么64位版本泄漏了这么多内存。我最终使用了MS工具来跟踪内存泄漏,比如DebugDiag和XPerf,Delphi 64位RTL中似乎存在一个基本缺陷,每次线程从DLL中分离时都会导致一些字节泄漏。对于必须全天候运行而不重新启动的高度多线程应用程序,此问题尤其重要 我用一个非常基本的项目重现了这个问题,该项目由一个主机应用程序和一个库组成,两者都是用XE2构建的。DLL与主机应用程序静态链接。主机应用程序创建只调用虚拟导出过程并退出的线程: 以下是库的源代码:线程关闭期间Win64 Delphi RTL内存泄漏?,delphi,memory-leaks,delphi-xe2,win64,Delphi,Memory Leaks,Delphi Xe2,Win64,很长一段时间以来,我注意到我的服务器应用程序的Win64版本泄漏内存。虽然Win32版本在相对稳定的内存占用下运行良好,但64位版本使用的内存会定期增加–可能是20Mb/天,没有任何明显的原因(不用说,FastMM4没有报告这两个版本的内存泄漏)。32位和64位版本的源代码相同。该应用程序是围绕Indy TIdTCPServer组件构建的,它是一个高度多线程的服务器,连接到一个数据库,该数据库处理由Delphi XE2生成的其他客户端发送的命令 我花了很多时间检查自己的代码,并试图理解为什么64
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个字节,这是完全可以接受的:
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
LeakTrack+13529
!Sysinit::alloctsbuffer+13
!Sysinit::InitThreadTLS+2b
!Sysinit::GetTls+22
!系统::分配器ISEFRAME+e
!系统::DelphiExceptionHandler+342
ntdll!RtlpExecuteHandlerForException+d
ntdll!RtlDispatchException+45a
ntdll!KiUserExceptionDispatch+2e
内核库!上升异常+39
!系统:::RaiseateExcept+106
!系统::RaiseExcept+1c
!系统::exitdl+3e
!系统:Halt0+54
!系统:::IB+123
!Sysinit:::InitLib+92
!智能::初始化+38
ntdll!LDRSHUTT下螺纹+155
ntdll!RtlExitUserThread+38
!系统::EndThread+20
!系统::类::ThreadProc+9a
!SystemThreadWrapper+36
内核32!BaseThreadInitThunk+d
ntdll!RtlUserThreadStart+1d
Remy Lebeau想了解发生了什么:
第二个漏洞看起来更像是一个明确的bug。线程期间
关机时,正在调用startIB(),这将调用ExitThreadTLS()以
释放调用线程的TLS内存块,然后调用Halt0()以
调用exitdl()以引发被捕获的异常
DelphiExceptionHandler()调用AllocateRaiseFrame(),其中
当它访问
名为ExceptionObjectCount的threadvar变量。这将重新分配资源
仍在进程中的调用线程的TLS内存块
被关闭的危险。因此,任何一个startIB()都不应该调用
DLL_线程_分离期间的Halt0(),或DelphiExceptionHandler应
当检测到错误时,不能调用AllocateRaiseFrame()
_正在引发TexitdlLexException
对我来说,似乎很清楚Win64处理线程关闭的方法中存在一个主要缺陷。此类行为禁止开发任何必须在Win64下运行27/7的多线程服务器应用程序
因此:
一个非常简单的解决方法是重新使用线程,而不是创建和销毁线程。线程非常昂贵,您可能也会得到性能提升。。。尽管调试很好…为了避免异常memoryleak陷阱,您可以尝试在FoobarProc周围放置一个try/except。也许不是为了一个最终的解决方案,而是为了看看为什么首先提出axception 我通常有这样的东西:
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;
我使用Delphi10.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中运行完全相同的代码不会泄漏任何内存
解决方案:用Exit交换Abort语句
结论:64位DLL中的线程执行函数不能留下异常(Abort也是异常),否则该异常会导致内存泄漏
至少这对我是有效的。“你们中有人对此问题有解决办法吗”我会使用32位应用程序,直到下一个稳定版本的带有64位编译器的delphi出现……如果我是你,我会将其缩减为最小尺寸的样本,显示泄漏,然后简单地提交给QC。@Who这里有同样的感觉,我希望我们是错的tho):有趣的是,似乎2009年已经报告了一个类似的错误(我想它是在Win32 RTL中):然而,它似乎已经被修复了,因为我的测试项目的Win32版本没有泄漏内存。@Cœur这是一个自动脚本,它只做我告诉它做的事情。我猜剩下的imageshack.us链接没有被检测为图像,我也不确定该链接是否正确