【问题标题】:Delphi application leaking AnsiStringsDelphi 应用程序泄漏 AnsiStrings
【发布时间】:2012-06-01 20:19:43
【问题描述】:

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

应用程序 (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 项目。是否有可能主要形式没有被正确破坏并因此留下悬垂的字符串?会这样吗?
  • 德尔福版本?如果可以,请使用 aqtime 进行测试,它会告诉您确切的泄漏位置。
  • @RichardA:正如你在 source\devcpp.dpr 中看到的,splashform 是使用“Free”释放的。将尝试将 caFree 添加到 OnClose 事件中。 @Warren:我非常怀疑 aqtime 能否告诉我比 FastMM4、gpProfiler 和 MemCheck 更多的信息。而且我也需要为 aqtime 升级到 XE(现在使用 D7)。我的大学确实有一个 D2009 许可证在某个地方浮动(但不适用于像我这样的 EE 人),但 aqtime 似乎甚至不支持它。
  • aqtime 在旧的 delphi 版本上运行良好,但是是的,您必须购买它。 AQTime 通过 XE2 与 Delphi 7 一起运行。

标签: string delphi memory memory-leaks dev-c++


【解决方案1】:

泄漏字符串的最常见方法是拥有一个包含字符串和指向该记录的指针的记录。如果您只是对该指针执行 Dispose(),编译器将只释放指针,而不是下面记录中的所有内容。始终确保您的处置代码告诉编译器您正在处置什么。

例如,假设在 TTreeView 中我将 PMyRecord = ^MyRecord 放入 Node.Data。如果最后你循环遍历所有节点并简单地执行Dispose(Node.Data),那么 MyRecord 中的任何字符串都将无法正确处理。

但是,如果您通过调用Dispose(PMyRecord(Node.Data)) 明确地告诉编译器指针的底层类型是什么来处理您的指针,那么就不会有内存泄漏。

【讨论】:

    【解决方案2】:

    我发现即使没有内存分配/指针操作,字符串(作为记录中的字段)也可能泄漏。

    这听起来很疯狂,但确实如此,至少在 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;
    

    【讨论】:

      【解决方案3】:

      对于简短的单词,Delphi 内置字符串类型是引用计数的。内存分配和 dispose 方法不负责更新引用计数,因此编译器不知道记录中的字符串实际上可以被释放。

      不鼓励使用引用计数字符串类型定义记录。我以前也有同样的困惑。如果您查看 Delphi 库的源代码。你会发现很多记录的 PChar 不是字符串。

      Some discuss about records

      【讨论】:

        【解决方案4】:

        您不需要显式分配字符串。除了使用引用计数进行修改外,对象或记录的字符串字段也可能泄漏。例如,

        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-------- ---------------------- 内存块已泄漏。尺寸为:12 分配此块时的堆栈跟踪(返回地址): 40305E 404B5D 404AF0 45C47B 43D726 42B0C3 42B1C1 43D21E 76C4702C [GetWindowLongW] 77AE3CC3 [RtlImageNtHeader 处的未知函数] 该块当前用于类对象:未知 分配编号为:484 从指针地址 7EF8DEF8 开始的 256 字节的当前内存转储: 01 00 00 ... ...

        现在您可以运行应用程序,暂停它然后搜索地址。对于上述日志和测试应用程序,地址解析为:

        分配此块时的堆栈跟踪(返回地址): 40305E -> _GetMem 404B5D -> _NewAnsiString 404AF0 -> _LStrAsg 45C47B -> TForm1.Button4Click(在 FreeMem 行) 43D726 -> TControl.单击 ...


        编辑: 无需手动查找地址,而是通过链接器选项生成详细的地图文件,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 的文本文件中得到这个想法。谢谢,会调查的。
        • 顺便说一句,如果您告诉链接器生成详细映射文件,那么这项工作会变得容易得多。然后 FastMM 可以为您进行查找。
        • @Mason - 谢谢,我想知道我在测试应用程序中缺少什么。 :)
        • @MasonWheeler 是的,成功了。从我已经能够收集到的几乎所有 跟踪可以追溯到传递给函数的常量。我将在主帖中添加一个示例。
        • @Orwell - 它看起来根本不像与传递给函数的常量相关的任何东西,请参阅我的答案中的更新。
        【解决方案5】:

        你说得对,应该自动清理字符串。不过,我已经看到了一些解决方法。

        第一个是如果你直接使用字符串数据结构做的事情会破坏引用计数。这是最有可能的,与您泄漏的字符串数量有关。

        另一个是调用 Halt 并将字符串引用留在堆栈上。但是你不会在堆栈上留下 40,000 个字符串引用,所以我会寻找传递字符串然后调整其引用计数的代码。

        【讨论】:

        • 我百分百确定我没有在任何地方使用 Halt。我经常使用 Exit,但这并不重要。嗯,不,例如,我不是在搞乱第零个索引。
        • @Orwell exit 在任何地方都可以安全使用:它将转到为任何本地字符串生成的隐藏 try..finally 块,并按预期处理引用计数。
        猜你喜欢
        • 2011-07-03
        • 2023-03-16
        • 2016-03-28
        • 2010-11-27
        • 2018-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多