首页 > 解决方案 > 连接的 Delphi 字符串是否保存在一个隐藏的临时变量中,该变量保留了对该字符串的引用?

问题描述

我正在尝试了解 Delphi 服务器应用程序中的内存问题:最初我怀疑是彻底泄漏,但现在相信我们看到的内存比应有的时间更长,因为编译器在动态连接字符串时使用了隐藏的临时 + ,导致痛苦的自由空间内存碎片。

背景:

这是 Windows 上的一套 32 位服务器应用程序,Delphi 版本很旧,我认为它是 7,但肯定是 Unicode 之前的版本,并使用 Nexus 3 内存管理器,我在其中编写了一个 DLL 来挂钩所有分配/free 调用(和千兆字节的内存跟踪)。

我有应用程序源代码,但没有编译器;我不是这个应用程序的开发人员(甚至不是 Delphi 开发人员),但我创建了广泛的自定义工具来监视、跟踪和分析内存。我一直在 IDA Pro 反汇编程序中挑选 .EXE。

一些示例代码:

我试图将其缩减到最低限度。此代码不打算编译:

procedure TaskThread.RunWorkLoop
begin
    while not Terminated do
    begin

      tsk := WaitForWorkToDo();  // this could sit for minutes at a time

      SetThreadName('Working on ' + tsk.Name);

      tsk.Run(); // THIS COULD TAKE A LONG TIME

      SetThreadName('Idle');
   end
end;

SetThreadName()接受一个 const 字符串参数并将其挂起,以便系统的其他部分知道该线程在做什么。

我对代码的反汇编显示编译器分配了一个隐藏的局部临时变量来接收“正在处理”和任务名称部分的连接,这就是传递给的内容SetThreadName,它还保留了字符串的句柄。

当任务正在运行时——这可能是 20 分钟——我相信字符串有两个句柄。一个是藏在里面SetThreadName,另一个是在隐蔽的临时。

这一切都很好。

然后,当任务结束并且线程名称设置为 时'Idle'SetThreadName()释放原始字符串并分配文字Idle

但是:我相信隐藏的本地临时文件仍然保留该字符串的句柄,refcount=1,所以它会占用空间,直到过程返回,或者下一个循环来覆盖隐藏的本地临时文件,释放旧值。

在此期间,程序无法访问它,无法显式释放,并且没有任何用处,但仍在消耗内存。

对于大多数程序来说,这无关紧要,因为它们开始和结束的时间相对较近,因此所有内容都会立即释放,但在循环服务器应用程序中,这些程序可能会停留更长时间。这导致我们内存碎片。

情况变得更糟

在实际应用中,更像是:

SetThreadName(tsk.Name + '-' + FormatDateTime('mm/dd/yy hh:nn:ss', Now));

在这种情况下,有两个隐藏的临时对象:一个用于 的结果FormatDateTime,另一个用于整体连接结果,实际上运行如下:

tmp1: String;
tmp2: String;
...
  tmp1 := FormatDateTime('...');
  tmp2 := tsk.Name + '-' + tmp1;
  SetThreadName(tmp2);

我确定我看到FormatDateTime任务完成后很长时间在内存中徘徊的字符串结果,我已经看到它实际上是一个约 30 字节的分配,位于 1 兆字节内存部分的中间,被包围可用空间; Nexus3MM 用于VirtualAlloc分配更大的操作系统级块。

该单个 30 字节字符串最终将在下一个循环或过程退出时释放,因此我确定这不是泄漏,但我宁愿将单个 30 字节分配放在一个孤独的中间当我们完成它时,兆字节部分实际上会消失,因此整个部分可以发布到操作系统。

但是,如果它存在的时间足够长,内存管理器就会从中分配其他东西,并且内存中的这个漏洞会变得更加永久。

我们有非常详细的忙/闲内存映射,并且确信这种碎片正在扼杀我们(这当然不是唯一的原因)。

我的问题:

1)我是否正确理解这一点?

2)如果是这样,是通过使用显式临时文件来消除隐藏临时文件的唯一解决方法,我们在其中执行以下操作:

tmp1: String;
tmp2: String;
...
  tmp1 := FormatDateTime('...');
  tmp2 := tsk.Name + '-' + tmp1;
  SetThreadName(tmp2);
  tmp1 := '';  // release the date/time string
  tmp2 := '';  // release the overall thread name string

我非常有信心我必须对FormatDateTime中间结果执行此操作(我已经专门看过它),但不确定整体串联。

这只是感觉不对。

编辑:几周后更新。我们重写了中央循环以使用显式临时变量,它实际上在一些关键服务器进程的内存碎片方面产生了明显的(尽管不是主要的)差异。我们还有其他事情要研究,但我很清楚这是一条值得走的路。

标签: stringdelphiconcatenation

解决方案


根据我的经验,它确实是这样工作的。我不确定这是通过合同还是通过实施。我想随着最近添加的内联变量声明,现在可能会略有不同。但在 unicode 之前的 Delphi 中,我相信它的工作原理与您描述的完全一样。

所有使用托管类型变量(隐式或显式)或包含变量的记录的例程都将在例程中生成一个隐式try/finally块,其中finally部分清除引用。你的代码真正做的是:

procedure TaskThread.RunWorkLoop
var
  sImplicit : string;
begin
  sImplicit := '';
  try
    while not Terminated do
    begin
      tsk := WaitForWorkToDo();  // this could sit for minutes at a time

      sImplicit := 'Working on ' + tsk.Name;

      SetThreadName(sImplicit);

      tsk.Run(); // THIS COULD TAKE A LONG TIME

      SetThreadName('Idle');
    end;
  finally
    sImplicit := '';
  end;
end;

在您的情况下,由于您从不退出使用隐式变量的例程,因此它确实保留在内存中。

至于解决方案,我相信您的建议会奏效。但是您也可以简单地将代码移动到另一个方法(或本地过程)。

procedure TaskThread.RunWorkLoop
  procedure JustKeepWorking;
  begin
    tsk := WaitForWorkToDo();  // this could sit for minutes at a time
    SetThreadName('Working on ' + tsk.Name);
    tsk.Run(); // THIS COULD TAKE A LONG TIME
    SetThreadName('Idle');
  end;
begin
  while not Terminated do
  begin
    JustKeepWorking;
  end
end;

此外,您可能需要检查此问题以获得更多见解。


推荐阅读