Delphi - 检查内存是不是“按时”释放

Posted

技术标签:

【中文标题】Delphi - 检查内存是不是“按时”释放【英文标题】:Delphi - Check if memory is being released "on time"Delphi - 检查内存是否“按时”释放 【发布时间】:2011-07-12 09:07:03 【问题描述】:

我有一个没有内存泄漏的 GUI 应用程序。我已经在多个测试周期中通过 FastMM 确认了这一点。 在一个特定客户端的服务器上,我遇到随机崩溃。服务器规格与我们其他客户端的规格非常一致(我们实际上已经在各种硬件上尝试过),程序使用的文件也是如此(据我所知,我有一些超级敏感的材料无法访问,但那里似乎没有任何异常)。

我已经尝试过 EurekaLog 和 MadShi 之类的工具,也许可以缩小问题的范围,但不幸的是,它们似乎只是偶尔在崩溃时捕获异常,而不是一直。当它发生时,它通常会在崩溃之前显示一个或多个“内存不足”错误。

所以我在想可能有些对象“太晚”被释放了,即只有在应用程序关闭时才释放,而不是在我打算释放它们的时候?我看过 FastMMUsageeTracker 演示,但无法真正理解这一切。任何地方都有文档吗?或者有人可以输入(有点容易理解的)单词,我该如何检查这个?

或者,检测应用程序正在接近其“内存限制”以采取一些预防措施的最佳方法是什么?如果我理解正确,一个普通的 Delphi 应用程序是 32 位的,它应该可以很好地处理高达 2Gb 的内存(当然前提是硬件支持它),对吗?

PS:Delphi 2009 或 XE,如果相关的话

谢谢!

编辑 - 问题可能已解决

我们发现了一个问题,即一段时间后会自动关闭并自行释放的弹出窗口的创建速度比消失的速度要快得多。随着时间的推移,这会消耗大量内存,然后任何内存分配基本上都会把它带到边缘并触发“内存不足”问题。

这可以解释为什么堆栈跟踪不一致。

我并不完全相信这是我们唯一的问题,因为即使不太可能,这种情况很可能在我们的应用程序运行多年之前就发生过,但不知何故并没有。我会在这个问题上做更多的挖掘。

感谢所有回复的人,每个答案实际上都包含有价值的信息。

【问题讨论】:

PS 内存不足异常可能发生在大约 1 GB 以上的任何地方 - 没有预定义的级别。似乎有很多因素会影响确切的阈值:总 RAM、总虚拟内存、其他进程正在使用多少等。另外,我下面的代码最初来自 FastMM 内存跟踪器! 有一点值得注意:在我从事的项目中,我从来没有设法耗尽可用内存,但我也遇到过几次内存不足的错误。在某些情况下,当您真正需要的只是几 K 或更少时,尝试请求内存管理器分配大小为几 GB 的单个缓冲区的损坏或计算错误的数据可能会导致此问题。 【参考方案1】:

如果您有 Delphi XE,它带有 AQTime,并且 AQTime 有一个内存分配分析器作为其技巧包的一部分。如果你在你的程序上运行它,你可能会看到你的 RAM 去哪儿了。

【讨论】:

谢谢梅森。我试过了(我对此寄予厚望),我还尝试了 Pro 版的试用。出于某种原因,我似乎无法让分配分析器工作。相反,它似乎在我要求它获得结果时挂起(我今天将它“获得结果”留了一个多小时 - 该应用程序有一个大约 100Mb 的工作集以防万一 - ,并且从未超过“试图获得结果”消息)。它对独立版本和集成版本都是一样的。我可以让性能分析器毫无问题地工作,但分配的却不行。 更新:我已经设法使用了独立的专业版 - 我在构建应用程序时忘记生成堆栈跟踪,现在我可以使用分配分析器。它向我指出了一个定时弹出窗口,该窗口被调用了数百或数千次,速度太快而无法通过它自动关闭的事实来弥补。仍然无法从集成 IDE 的版本中获得结果(???)。 标记为答案,因为这就是我实际发现弹出问题的方式。 @Bourgui:很高兴你能找到你的问题。 :)【参考方案2】:

忘掉“Windows”内存吧——你想要的是应用程序分配的实际内存。这是您可以判断您分配的内存是否随着时间的推移未被释放的唯一方法。对于带有 FastMM 的 Delphi 2006+,这就是您所需要的:

//------------------------------------------------------------------------------  
// CsiGetApplicationMemory  
//  
// Returns the amount of memory used by the application (does not include  
// reserved memory)  
//------------------------------------------------------------------------------  
function CsiGetApplicationMemory: Int64;  
var  
  lMemoryState: TMemoryManagerState;  
  lIndex: Integer;  
begin  
  Result := 0;  

  // get the state  
  GetMemoryManagerState(lMemoryState);  

  with lMemoryState do begin  
    // small blocks  
    for lIndex := Low(SmallBlockTypeStates) to High(SmallBlockTypeStates) do  
      Inc(Result,  
          SmallBlockTypeStates[lIndex].AllocatedBlockCount *  
          SmallBlockTypeStates[lIndex].UseableBlockSize);  

    // medium blocks  
    Inc(Result, TotalAllocatedMediumBlockSize);  

    // large blocks  
    Inc(Result, TotalAllocatedLargeBlockSize);  
  end;  
end;  

我将这个时间间隔(10 秒到 10 分钟之间的任意时间)记录到我的日志文件中,以及与上次的差异。

【讨论】:

+1 这样的事情也是我的第一步。如果您排除其他所有因素,请首先确定您的应用是否在某个列表中占用内存,或者它是否是“真正的”堆碎片。 谢谢 Misha,我还没有尝试过,但它看起来很有趣,我会试一试 - 在我只看小块之前。【参考方案3】:

您可以了解您的应用程序正在使用多少内存 - 请参阅此 About 页面。总结:

uses PsAPI;

//current memory size of the current process in bytes
function CurrentMemoryUsage: Cardinal;
var
  pmc: TProcessMemoryCounters;
begin
  pmc.cb := SizeOf(pmc) ;
  if GetProcessMemoryInfo(GetCurrentProcess, @pmc, SizeOf(pmc)) then
    Result := pmc.WorkingSetSize
  else
    RaiseLastOSError;
end;
ShowMessage(FormatFloat('Memory used: ,.# K', CurrentMemoryUsage / 1024)) ;

如果您定期在服务器中记录该值,您至少会了解正在发生的事情。该结果中有更多信息可以帮助您更多地了解您的程序正在做什么。

解决方法是查看实际使用内存的内容并更积极地进行管理。我怀疑你会在某个地方创建对象并且只在关闭时释放它们,当你完成它们后你可以(并且应该)释放它们。

一种可能的解决方法是在完整版 FastMM 上使用 /3GB 开关,看看问题是否需要更长的时间才会出现。

如果你非常倒霉,你将“破坏”FastMM 的内存池管理算法,因此它永远不会释放内存 (a related question)。尝试不同的内存管理器可能会对您有所帮助,因为有些管理器会更积极地回收未使用的内存。但是,如果你正在分割你的堆,唯一真正的解决方案是找出如何避免这样做。这是一个复杂的话题,所以再说一遍:先尝试上面的简单事情。

【讨论】:

谢谢莫兹。我已经开始玩弄 TProcessMemoryCounters,但是,这不是“仅”提供工作集内存,也就是 RAM 内存吗?只是在这里猜测,但“内存不足”错误不会表明整体虚拟内存太大了吗?如果我错了纠正我。我会试试 /3G 开关。 @Bourgui,“内存不足”是一个相当广泛的信息,现代机器应该有(远)超过 2GB 的物理内存,所以你不应该耗尽虚拟内存。 ProcessMemoryCounters 结构中包含其他信息,我会记录全部内容并查看发生了什么变化。日志记录还将帮助您找出问题的确切位置。我建议还从您的异常中查看堆栈跟踪。在最坏的情况下,如果您发现您的日志记录因内存不足而失败,您可能必须编写一个外部记录器。但在需要之前避免这样做。 感谢您的反馈。我也是这么想的。不幸的是,我的大部分堆栈跟踪都没有引导我到任何地方,而且大多不一致。从那以后,我能够隔离一个问题(请参阅我的 OP 编辑​​) - 希望它是我所有问题的根源。【参考方案4】:

当您收到错误时,能否向我们显示堆栈跟踪?出错时消耗了多少内存?

我前段时间制作了一个内存记录器(用于 FastMM),它记录了 2 个点之间的所有内存(使用“startlog”和“endlog”过程)以查找“软泄漏”:我在列表中分配对象但是从不清除列表,仅在关闭应用程序时,所以 FastMM 报告没有泄漏。通过使用我的内存记录器,我可以找到这些对象(它只记录在执行“endlog”过程之前未释放的新分配内存)。 我看看能不能找到这段代码。

顺便说一句:您可以通过其他 3 种方式获得“内存不足”:

FastMM 在分配时检测到错误时会给出此错误。所以不是真正的“内存不足”,而是内部 fastmm 错误(由于损坏等) 分配一个非常大的块(例如 1Gb)。我曾经因为流读取错误(RemObjects)而得到这个,所以它为字符串读取了错误的大小值,所以它试图预先分配一个(随机)大字符串。这样的错误看起来很奇怪,因为在我的情况下,我的应用分配了大约 150Mb,所以也没有真正的“内存不足” 碎片:如果您尝试分配一个 10Mb 的块,但 Windows 找不到一个连续的 10Mb 块,则 Windows 将给出“内存不足”。

所以请给出错误发生时的堆栈跟踪和使用的内存量!

【讨论】:

您好安德烈,感谢您的回复。我没有发布堆栈跟踪,因为它们都是不同的。但是,我们可能已经发现了问题,请参阅我的编辑。【参考方案5】:

由于 Delphi 对 32 位地址空间的限制,此类问题变得越来越普遍。

您可以做的第一件事也是最简单的事情是在 64 位操作系统上运行,然后从 2GB 可用地址空间(就像您在 32 位操作系统上一样)移动到 4GB 地址。这不会自动发生。您需要将您的应用程序标记为 LARGEADDRESSAWARE。通过将以下内容添加到您的 .dpr 文件来执行此操作:

const
  IMAGE_FILE_LARGE_ADDRESS_AWARE = $0020;

$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE

内存不足错误的另一个常见原因不是内存不足,而是您要求大块连续内存,但没有可用的单个连续地址空间块。

处理这个问题比较困难。您首先需要确定当前需要大量连续内存块的代码部分。接下来,您必须修改正在这样做的对象,并安排它们改为请求小块内存,然后您可以“缝合在一起”以呈现更大块的外观。根据我的经验,这通常发生在使用动态数组的代码中。

【讨论】:

谢谢大卫。问题:这与 FastMM 的 /3G 标志有什么区别?正如我在编辑中提到的那样,我发现了一个问题,这可能是我的麻烦之源,因为我的大部分内存分配都相对较小(它们不超过 10 兆字节)并且我们的应用程序正在系统上运行这没有被用于其他任何事情,我怀疑(并希望!)这是这里的原因。更不用说我倾向于尽可能回避动态数组。 @Bourgui 10s of Mb,重复可能是个问题。 /3G 是 32 位版本。您仍然需要将您的 exe 标记为 LARGEADDRESSAWARE。然后你需要用 /3GB 启动 windows。有人这样做吗?无论如何,您客户的服务器肯定是 64 位机器。您是否将您的应用程序标记为 LARGEADDRESSAWARE?如果没有,为什么不呢? 如此大的内存分配在应用程序中相对较少而且相差甚远,这只是它可以达到的最大值。它们也是时间持久的。这些分配也有很好的记录,我们应该能够相对容易地注意到这些问题。这就是为什么我不认为这是问题所在。但我还没有完全排除任何事情。事实上,到目前为止,我们从来没有遇到过内存问题,所以我们从来没有动力去开发我们的应用程序 LargeAddressAware,但这绝对是值得研究的事情——我无法为 Delphi 64 位编译器屏住呼吸! ;o) 嗯,听起来您的代码中可能存在错误,而不是真正的内存问题。【参考方案6】:

当我收到“内存不足”错误时,这是​​由于循环失控。循环通常会分配内存并且在所有可用内存都用完之前不会停止。释放内存不是问题,因为程序永远不会到那个点。被我咬过的代码类型有:

一个没有“x.Next”的“while not x.Eof do”循环来推进数据集,或者

从未遇到退出条件的递归过程或子例程。

我会寻找在某些情况下可以“永远”持续下去的任何类型的循环或递归,例如在内存中构建一个庞大的数据结构。

【讨论】:

感谢 crefird,但我不认为这里是这种情况。请参阅我的编辑以获取有关我们目前发现的更多信息。

以上是关于Delphi - 检查内存是不是“按时”释放的主要内容,如果未能解决你的问题,请参考以下文章

Delphi Setlength 内存释放总结

Delphi Setlength 内存释放总结

delphi 窗体的创建和释放

delphi 窗体的创建和释放

[原创]Delphi FreeAndNil 是一个过程,并不是函数

DELPHI 用TThread如何判断线程是不是存在 并且释放线程