Delphi应用程序泄漏AnsiStrings

12

根据FastMM4的报告,我目前正在处理的Delphi程序泄漏了很多字符串,准确地说是AnsiStrings类型:

enter image description here

这个应用程序(http://sourceforge.net/projects/orwelldevcpp/)以前泄漏了更多其他数据类型,但是FastMM4可以报告实例的创建位置,因此我设法修复了那些泄漏。奇怪的是,FastMM4根本没有报告这些泄漏的位置。

编辑:看起来它终究是报告了,见回答中的解决方法。无论如何,问题仍然存在:我到底是如何泄漏这些东西的?

所以,呃,不幸的是,我不知道要寻找什么。我的意思是,如果这些变量超出范围,它们应该被自动释放,对吧(即使它们在堆上)?

我通过随机注释并观察计数会发生什么,找到了一些泄漏的迹象。下面是一个示例:

// simply passing it a constant creates a leak...
MainForm.UpdateSplash('Creating extra dialogs...');

procedure TMainForm.UpdateSplash(const text : AnsiString);
begin
  if not devData.NoSplashScreen then // even if this branch is NOT taken
    SplashForm.Statusbar.SimpleText := 'blablabla' + text;
end;

// And even if the function call itself is placed within a NOT taken branch!

这是另一个内存泄漏的例子:

// Passing this constants produces leaks...
procedure TCodeInsList.AddItemByValues(const a, b, c: AnsiString;...);
var
  assembleditem : PCodeIns;
begin
   new(assembleditem);
   assembleditem^.Caption:=a;
   assembleditem^.Line:=b;
   assembleditem^.Desc:=c;
   ...
   fList.Add(assembleditem);
end;

// ... even when calling this on WM_DESTROY!
destructor TCodeInsList.Destroy;
var
  I: integer;
begin
  for I := 0 to fList.Count - 1 do
    Dispose(fList[I]);
  fList.Free;
  inherited Destroy;
end;

// produces leaks!?

这里有很多关于字符串泄漏的问题,但没有一个真正阐明了应该寻找哪些模式。Google也没有提供相关信息。

编辑:所以,我必须寻找传递的常量。但是为什么?

那么,有任何想法吗?


我现在无法加载sourceforge项目。有没有可能主窗体没有被正确销毁,从而留下了悬空的字符串?那会导致这种情况吗? - Richard A
Delphi的版本?如果可以的话,使用AQtime进行测试,它会准确地告诉您泄漏的位置。 - Warren P
@RichardA:如您看到的 source\devcpp.dpr中,splashform 是使用 'Free' 释放的。我会尝试在 OnClose 事件中添加 caFree。<crlf> @ Warren:我非常怀疑 aqtime 能告诉我比 FastMM4、gpProfiler 和 MemCheck 更多的信息。而且我需要升级到 XE 版本才能使用 aqtime (现在使用的是 D7)。我的大学确实有一个 D2009 许可证漂浮在某个地方(不过对像我这样的 EE 人来说是没有的),但似乎 aqtime 连那个都不支持。 - Orwell
AQTime在旧版本的Delphi上运行良好,但是需要购买。AQTime可以与Delphi 7到XE2一起运行。 - Warren P
5个回答

14

你无需显式分配字符串。除了与引用计数搞作之外,对象或记录的字符串字段也可能泄漏。例如,

type
  PRecord = ^TRecord;
  TRecord = record
    S: string;
  end;

procedure TForm1.Button4Click(Sender: TObject);
var
  r: PRecord;
begin
  GetMem(r, SizeOf(r^));
  Initialize(r^);
  r.S := ' ';
  FreeMem(r);
在上面的例子中,由于记录本身的内存已被释放,因此FastMM仅报告泄漏的字符串。
无论如何,FastMM在对话框中不显示堆栈跟踪并不意味着它缺少该信息。请确保在“FastMM4Options.inc”中定义了FullDebugModeLogMemoryLeakDetailToFileLogErrorsToFile。然后在可执行文件目录中查找一个“[ExecutableName]_MemoryManager_EventLog.txt”文件。
对于上面的示例,FastMM会生成以下文件:
--------------------------------2012/5/27 4:34:46-------------------------------- A memory block has been leaked. The size is: 12
Stack trace of when this block was allocated (return addresses): 40305E 404B5D 404AF0 45C47B 43D726 42B0C3 42B1C1 43D21E 76C4702C [GetWindowLongW] 77AE3CC3 [Unknown function at RtlImageNtHeader]
The block is currently used for an object of class: Unknown
The allocation number is: 484
Current memory dump of 256 bytes starting at pointer address 7EF8DEF8: 01 00 00 ... ...
现在您可以运行应用程序,暂停它,然后搜索地址。对于上面的日志和测试应用程序,这些地址解析为:
Stack trace of when this block was allocated (return addresses): 40305E -> _GetMem 404B5D -> _NewAnsiString 404AF0 -> _LStrAsg 45C47B -> TForm1.Button4Click (on FreeMem line) 43D726 -> TControl.Click ... 编辑: 通过链接器选项生成详细的映射文件,而不是手动查找地址,FastMM将会自行完成(感谢Mason的评论)。
您在问题上的编辑反映了与上面示例中非常相似的泄漏。如果'fList'是普通的TList,它只保存指针并不知道这些指针指向什么。因此,在释放指针时,仅释放为指针本身分配的内存,而不是记录的字段。因此,泄漏与传递给函数的常量无关,而是如下模式:
var
  assembleditem: PCodeIns;
  p: Pointer;
begin
  new(assembleditem);
  assembleditem^.Caption:='a';
  ..    
  p := assembleditem;
  Dispose(p);

为了释放该记录,代码应将指针强制转换为其类型:

Dispose(PCodeIns(p));

所以你的 'TCodeInsList.Destroy' 应该是:

destructor TCodeInsList.Destroy;
var
  I: integer;
begin
  for I := 0 to fList.Count - 1 do
    Dispose(PCodeIns(fList[I]));
  fList.Free;
  inherited Destroy;
end;


最后,你要找的模式似乎是在寻找代码意图释放具有字符串字段的记录(较少对象)的地方。查找 Dispose ,稍微不太可能是 FreeMem ,更不太可能是 FreeInstance 以释放 FastMM 显示为分配的内存泄漏的对象/记录的内存可能会有所帮助。


嗯,当我在寻找其他泄漏时,可能忽略了它,但是没错,FastMM4确实显示了一些信息:102DF8 [SynEditKeyCmds] [SynEditKeyCmds] [@GetMem]。将其乘以40000,在一个150MiB的文本文件中就能得到大致的想法。谢谢,我会研究一下的。 - Orwell
1
顺便提一下,如果您告诉链接器生成详细的映射文件,那么这项工作将变得更加容易。然后 FastMM 可以为您执行查找操作。 - Mason Wheeler
@Mason - 谢谢,我在测试应用程序中缺少什么一直在想。 :) - Sertac Akyuz
@MasonWheeler 嗯,那就解决了。从我所能收集到的信息来看,几乎所有的痕迹都可以追溯到传递给函数的常量。我会在主贴中添加一个例子。 - Orwell
@Orwell - 看起来这与传递给函数的常量完全无关,请查看我回答中的更新。 - Sertac Akyuz
@SertacAkyuz:嗯,几乎所有这些字符串泄漏都与缺少转换有关,因此它不知道这些指针指向什么数据结构。谢谢您的信息! - Orwell

4
您说得没错,字符串应该自动清理。然而,我见过一些方法会弄乱这个过程。
第一种情况是,如果您直接使用字符串数据结构进行操作,则可能会破坏引用计数。鉴于您泄漏的字符串数量,这种情况最有可能发生。
另外一种情况是,在调用 Halt 时,在堆栈上留下字符串引用。但是,您不会在堆栈上留下 40,000 个字符串引用,所以我会寻找那些传递一个字符串然后再修改其引用计数的代码。

我百分之百确定我没有在任何地方使用Halt。我经常使用Exit,但那应该不重要。嗯,不,例如我没有搞乱零索引。 - Orwell
4
exit 可以安全地在任何地方使用:它会进入为任何本地字符串生成的隐藏的 try..finally 块,并按预期处理引用计数。 - Arnaud Bouchez

1
最常见的泄漏字符串的方式是有一个记录包含一个字符串和指向该记录的指针。如果你仅仅对该指针执行Dispose(),编译器只会释放指针,而不是底层记录中的所有内容。请确保您的dispose代码告诉编译器您正在处理的内容。
例如,假设在TTreeView中我将PMyRecord = ^MyRecord放入Node.Data中。如果最后您循环遍历所有节点并简单地执行Dispose(Node.Data),则MyRecord中的任何字符串都不会被正确处理。
但是,如果您通过调用Dispose(PMyRecord(Node.Data))明确告诉编译器指针的底层类型,那么就不会有内存泄漏。

1
简言之,Delphi 内置的字符串类型是引用计数的。内存分配和释放方法不负责更新引用计数,因此编译器不知道记录中的字符串实际上可以被释放。
定义具有引用计数字符串类型的记录是不鼓励的。我以前也有同样的困惑。如果你查看 Delphi 库的源代码,你会发现许多记录都有 PChar 而不是字符串。 关于记录的一些讨论

0

我发现一个字符串(作为记录中的字段)甚至在没有内存分配/指针操作的情况下也可能泄漏。

听起来很疯狂,但至少在XE3中是真的。以下是示例:

TMyRecord = record
x,
y: integer;
s: ansistring;
end;

function GetMyRec: TMyRecord;
begin
....
end;

....
procedure DoSomething;
var
  rec: TMyRecord;
begin
  ...
  rec := GetMyRec; //First call - everything is OK
  ...
  rec := GetMyRec; //Repeated call > Memory Leak of 
                   //Ansistring !!!!
  //To avoid the leak do the following BEFORE a 
  //repeated call: rec.s := unassigned;
end;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接