我试图了解 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()
接受一个常量字符串参数并挂起它,以便系统的其他部分知道该线程正在做什么。
我对代码的反汇编表明,编译器已分配了一个隐藏的局部临时变量来接收“正在处理”和任务名称部分的串联,这就是传递给的内容SetThreadName
,其中还保留了字符串的句柄。
当任务正在运行时 - 这可能是 20 分钟 - 我相信有two字符串的句柄。其中一个被保存在SetThreadName
,另一个是暂时隐藏的。
这一切都很好。
然后,当任务结束并且线程名称设置为'Idle'
, SetThreadName()
释放原始字符串并分配文字Idle
.
但是:我相信隐藏的本地临时仍然保留该字符串的句柄,并且 refcount=1,因此它将占用空间,直到过程返回,或者next循环会覆盖隐藏的本地临时值,释放旧值。
在此期间,程序无法访问它,无法显式释放它,并且没有任何有用的目的,但仍在消耗内存。
对于大多数过程来说,这并不重要,因为它们的开始和结束彼此相对接近,因此所有内容都会立即释放,但在循环服务器应用程序中,这些可以保留更长时间。这导致我们的内存碎片。
情况变得更糟
在实际应用中,更多的是这样的:
SetThreadName(tsk.Name + '-' + FormatDateTime('mm/dd/yy hh:nn:ss', Now));
在这种情况下,有two隐藏的临时变量:一个用于结果FormatDateTime
,另一个用于整体串联结果,实际上运行为:
tmp1: String;
tmp2: String;
...
tmp1 := FormatDateTime('...');
tmp2 := tsk.Name + '-' + tmp1;
SetThreadName(tmp2);
我确信我看到了字符串结果FormatDateTime
任务完成后很长一段时间都在记忆中徘徊,我已经看到了字面上地是位于 1 MB 内存部分中间的单个 ~30 字节分配,周围有可用空间; Nexus3MM用途VirtualAlloc
分配更大的操作系统级块。
那个 30 字节的字符串will最终会在下一个循环或程序退出时被释放,所以我确定它不是leak,但我希望当我们完成它时,位于孤独的 1 MB 部分中间的单个 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
中间结果(我已经具体看到过),但不确定整体串联。
这只是感觉不对。
编辑:几周后只是更新。我们重写了中央循环以使用显式临时变量,这实际上对某些关键服务器进程的内存碎片产生了明显的(尽管不是主要的)差异。我们还有其他事情需要研究,但我很清楚这是一条值得走的路。