很长一段时间以来,我注意到我的服务器应用程序的 Win64 版本存在内存泄漏问题。虽然 Win32 版本工作正常,内存占用相对稳定,但 64 位版本使用的内存却定期增加 – 可能 20Mb/天,没有任何明显的原因(不用说,FastMM4 没有报告两者的任何内存泄漏) 。 32 位和 64 位版本的源代码是相同的。该应用程序是围绕 Indy TIdTCPServer 组件构建的,它是一个连接到数据库的高度多线程服务器,用于处理由 Delphi XE2 编写的其他客户端发送的命令。
我花了很多时间检查自己的代码并试图理解为什么 64 位版本会泄漏如此多的内存。我最终使用了旨在跟踪内存泄漏的 MS 工具,例如 DebugDiag 和 XPerf,而且 Delphi 64 位 RTL 中似乎存在一个基本缺陷,导致每次线程从 DLL 分离时都会泄漏一些字节。对于必须 24/7 运行而无需重新启动的高度多线程应用程序,此问题尤其严重。
我用一个非常基本的项目重现了这个问题,该项目由主机应用程序和库组成,两者都是用 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 个字节,这是完全可以接受的:
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
雷米·勒博了解发生了什么:
第二次泄漏看起来更像是一个明确的错误。线程期间
shutdown,StartLib() 被调用,它调用 ExitThreadTLS() 来
释放调用线程的 TLS 内存块,然后调用 Halt0() 来
调用 ExitDll() 引发异常,该异常被捕获
DelphiExceptionHandler() 调用 AllocateRaiseFrame(),其中
当它访问一个线程时,间接调用 GetTls() 并因此调用 InitThreadTLS()
名为 ExceptionObjectCount 的 threadvar 变量。这会重新分配
仍在进程中的调用线程的 TLS 内存块
被关闭。所以 StartLib() 不应该被调用
在 DLL_THREAD_DETACH 期间停止 0(),或者 DelphiExceptionHandler 应该
当检测到时不调用 AllocateRaiseFrame()
引发 _TExitDllException。
对我来说,很明显 Win64 处理线程关闭的方式存在一个重大缺陷。这种行为禁止开发任何必须在 Win64 下 27/7 运行的多线程服务器应用程序。
So:
- 你觉得我的结论怎么样?
- 你们中有人有解决这个问题的方法吗?
质检报告105559 https://web.archive.org/web/20171220162905/http://qc.embarcadero.com/wc/qcmain.aspx?d=105559