我试图了解Delphi服务器应用程序中的内存问题:最初我怀疑彻底泄漏,但是现在相信,由于编译器在使用+动态连接字符串时使用了隐藏的临时文件,因此内存的闲置时间比应有的更长。 ,造成痛苦的自由空间内存碎片。
这是Windows上的一整套32位服务器应用程序,Delphi版本已经很老了,我认为它是7,但可以肯定是Unicode之前的版本,并使用Nexus 3内存管理器在其中编写了DLL来挂接所有分配/ free调用(以及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
。
但是:我相信隐藏的本地临时文件仍然保留该字符串的句柄,且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
分配更大的OS级块。
这单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
中间结果(我已经看到了)来完成此操作,但是不确定整体串联。
这只是感觉不对。
编辑:几周后才更新。我们已经重写了中央循环以使用显式的临时性,实际上它在某些关键服务器进程的内存碎片化方面产生了明显的(尽管不是主要的)差异。我们还有其他事情需要研究,但是对我来说很明显,这是一条值得走的路。
根据我的经验,它确实可以像这样工作。我不确定这是通过合同还是通过实施。我猜想随着最近增加的内联变量声明,现在可能有所不同。但是我相信在preunicode的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;
另外,您可能需要检查此问题以获取其他见解。