线程关闭期间Win64 Delphi RTL中的内存泄漏?

很长一段时间我注意到我的服务器应用程序的Win64版本泄漏了内存。 虽然Win32版本在相对稳定的内存占用情况下工作正常,但64位版本使用的内存有规律地增加 - 可能为20Mb /天,没有任何明显的原因(不用说,FastMM4没有报告任何内存泄漏) 。 源代码在32位和64位版本之间是相同的。 该应用程序是围绕Indy TIdTCPServer组件构建的,它是一个高度多线程的服务器,连接到一个数据库,用于处理由Delphi XE2制作的其他客户端发送的命令。

我花了很多时间阅读我自己的代码,并试图理解为什么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显示泄漏的截图(查看名为“Heap”的红线)。 以下屏幕截图是在30分钟内完成的。

32位二进制显示增加了16个字节,这是完全可以接受的:

32位版本的内存使用情况http://img401.imageshack.us/img401/6159/soleak32.png

64位二进制显示增加了12476字节(从820K增加到13296K),这更成问题:

64位版本的内存使用情况http://img12.imageshack.us/img12/209/soleak64.png

堆内存的不断增加也由XPerf确认:

XPerf用法http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

使用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帮助我在Embarcadero论坛上了解正在发生的事情:

第二次泄漏看起来更像是一个确定的错误。 在线程关闭期间,调用了StartLib(),它调用ExitThreadTLS()释放调用线程的TLS内存块,然后调用Halt0()调用ExitDll()以引发DelphiExceptionHandler()捕获的异常,以调用AllocateRaiseFrame ),它在访问名为ExceptionObjectCount的threadvar变量时间接调用GetTls()和InitThreadTLS()。 这将重新分配正在关闭的调用线程的TLS内存块。 因此,StartLib()不应该在DLL_THREAD_DETACH期间调用Halt0(),或者DelphiExceptionHandler在检测到引发_TExitDllException异常时不应该调用AllocateRaiseFrame()。

对我来说,在Win64方式中处理线程关闭有一个主要缺陷。 这种行为禁止开发必须在Win64下运行27/7的多线程服务器应用程序。

所以:

  • 你对我的结论有什么看法?
  • 你们有没有解决这个问题的方法?
  • 测试源代码和二进制文件可以在这里下载。

    感谢您的贡献!

    编辑 :质检报告105559.我在等你的投票:-)


    一个非常简单的解决方法是重新使用该线程,而不是创建并销毁它们。 线程是非常昂贵的,你可能会得到一个性能提升...在调试虽然......的荣誉

    链接地址: http://www.djcxy.com/p/66921.html

    上一篇: Memory leak in the Win64 Delphi RTL during thread shutdown?

    下一篇: Anatomy of a "Memory Leak"