我正在尝试理解Delphi服务器应用程序中的内存问题:最初我怀疑是明显的泄漏,但现在认为我们看到的内存挂起时间比应该更长,因为编译器在使用+动态拼接字符串时使用了隐藏的临时内存,导致令人痛苦的自由空间内存碎片化。
背景:
这是一套32位Windows服务器应用程序,Delphi版本非常古老,我认为它是7,但肯定是pre-Unicode,并使用Nexus 3内存管理器,在那里我编写了一个DLL来挂钩所有的分配/释放调用(和GB级别的内存跟踪)。
我有应用程序源代码,但没有编译器;我不是这个应用程序的开发人员(甚至不是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
赋值给它。但是:我认为隐藏的本地临时变量仍然保留对该字符串的引用,引用计数为1,因此它将占用空间,直到过程返回或下一个循环覆盖该隐藏的本地临时变量,释放旧值。
在此期间,它不可访问程序,无法被显式释放,也没有任何有用的目的,但仍在消耗内存。
对于大多数过程而言,这并不重要,因为它们开始和结束的时间相对接近,所以一切都会同时释放,但在循环服务器应用程序中,这些可能会存在更久。这导致了内存碎片化。
情况更糟
实际应用程序更多地是这样的:SetThreadName(tsk.Name + '-' + FormatDateTime('mm/dd/yy hh:nn:ss', Now));
在这种情况下,有两个隐藏的临时变量:一个用于存储FormatDateTime
的结果,另一个用于整个拼接结果,实际上相当于运行以下代码:
在这种情况下,有两个隐藏的临时变量:一个用于FormatDateTime
函数的结果,另一个用于整个字符串拼接的结果,等效地运行如下代码:
tmp1: String;
tmp2: String;
...
tmp1 := FormatDateTime('...');
tmp2 := tsk.Name + '-' + tmp1;
SetThreadName(tmp2);
我确定我看到的是FormatDateTime
字符串结果持续留在内存中,即使任务完成后,我曾经看到它是一个单独的 ~30字节分配,坐落在1兆字节内存区域的中间,并被空闲空间所包围;Nexus3MM使用VirtualAlloc
来分配更大的操作系统级别的块。
那个单独的30字节字符串最终会被释放,要么在下一次循环时,要么当过程退出时,因此我确定它不是一个泄漏,但我希望当我们完成它时,那个单独的 30字节分配能从孤立的1兆字节区域中消失,以便整个区域可以释放给操作系统。
但是如果它停留得足够长,内存管理器将从中分配其他内容,而这个内存空洞变得更加永久。
我们有非常详细的繁忙/空闲内存映射,并确信这种碎片化正在摧毁我们(这绝对不是唯一的原因)。
我的问题:
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
中间结果来完成这个任务(我已经看到了具体的内容),但整个串联还不确定。
这感觉很不对劲。
编辑:几周后的更新。我们重写了中心循环以使用显式临时变量,这实际上在一些关键服务器进程的内存碎片化方面产生了明显(尽管不是很大)的差异。我们仍然有其他事情要处理,但对我来说,这是值得一试的方法。