Delphi - 如何检查内存是否及时释放?

7
我有一个GUI应用程序,没有内存泄漏问题。我已经通过FastMM进行了多次测试,并确认了这一点。
在某个特定客户端的服务器上,我会遇到随机崩溃的情况。服务器规格与我们其他客户端的相同(我们实际上尝试过各种硬件),程序使用的文件也是如此(据我所知,有些超级敏感的材料我无法访问,但似乎没有什么异常)。
我尝试过EurekaLog和MadShi等工具,以缩小问题范围,但不幸的是,它们只在偶尔发生崩溃时捕获异常,而不是每次都能捕获。当出现异常时,通常会显示一个或多个“内存不足”的错误,然后才会崩溃。
所以我想也许一些对象被“太晚”释放,即只有在应用程序关闭时才释放,而不是我想要释放它们的时候?我看过FastMMUsageeTracker演示文稿,但并不能真正理解其中的内容。有没有文档可以参考?或者有人能用(比较易懂的)话语告诉我如何检查这个问题?
另外,检测应用程序是否接近其“内存限制”的最佳方法是什么,以便采取一些预防措施?如果我正确理解,一个常规的Delphi应用程序是32位的,它应该可以处理多达2Gb的内存(当然前提是硬件支持),对吗?
PS:Delphi 2009或XE,如果有关的话。
谢谢!
编辑-问题可能已经解决
我们能够找到一个问题,即弹出窗口在一段时间后自动关闭和释放自身的速度远远快于其消失的速度。这会随着时间的推移消耗大量内存,然后任何内存分配基本上都会使其超过极限并触发“内存不足”问题。
这就解释了为什么堆栈跟踪不一致。
我并不完全相信这是我们唯一的问题,因为尽管不太可能,在我们的应用程序运行多年之前可能已经发生过这种情况,但某种程度上它还没有发生。我将在此问题上进行更多的挖掘。
感谢所有回答的人,每个答案实际上都有宝贵的信息。

1
PS 内存不足异常可能发生在大约1 GB及以上的任何地方 - 没有预定义的级别。 有许多因素似乎会影响确切的阈值:总RAM,总虚拟内存,其他进程使用了多少等。 此外,我下面的代码最初来自于FastMM内存跟踪器! - Misha
1
值得注意的一件事是:在我所参与的项目中,我从未成功地耗尽可用内存,但有几次出现了内存不足的错误。在某些情况下,这可能是由于损坏或计算不当的数据试图请求内存管理器分配几个千兆字节大小的单个缓冲区时引起的,而实际上你只需要几K或更少的空间。 - Mason Wheeler
6个回答

7
如果你有Delphi XE,它自带AQTime,而AQTime作为其技巧包的一部分具有内存分配分析器。如果你在程序上运行它,你可能能够看到你的RAM使用情况。

1
谢谢Mason。我尝试了那个方法(我对它寄予了很大的希望),我还试用了专业版的试用版。由于某种原因,我似乎无法让分配分析器正常工作。相反,当我要求它获取结果时,它似乎会挂起(今天我让它“获取结果”超过一个小时 - 应用程序的工作集大小约为100Mb,如果这有关的话 - ,并且从未超过“正在尝试获取结果”的消息)。独立版本和集成版本都是如此。我可以轻松地让性能分析器正常工作,但不能让分配分析器正常工作。 - Bourgui
更新:我已经成功使用了独立的专业版 - 我忘记在构建我的应用程序时生成堆栈跟踪,现在我能够使用分配分析器。它指向一个被调用了数百或数千次的定时弹出窗口,关闭速度太快,无法通过自动关闭来补偿。但是,我仍然无法从 IDE 集成版本中获取结果(???)。 - Bourgui
标记为答案,因为这就是我发现弹出问题的方式。 - Bourgui
@Bourgui:很高兴你找到了问题的解决方案。 :) - Mason Wheeler

6

不要考虑“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 像这样的方法也是我的第一步。如果你排除了其他所有可能性,首先要找出你的应用程序是否在某个列表中占用了内存,或者它是否存在“真正”的堆碎片问题。 - Marco van de Voort
谢谢Misha,我还没有尝试过,但看起来很有趣,我会试一试 - 之前我只看了小块。 - Bourgui

3

您可以查找应用程序正在使用的内存量 - 请参阅此关于页面。摘要:

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的内存池管理算法,以便它永远不会释放内存(相关问题)。尝试不同的内存管理器可能会帮助您,因为其中一些更积极地回收未使用的内存。但是,如果您正在分段堆,则唯一真正的解决方案是找出如何避免这样做。这是一个复杂的主题,因此请先尝试以上简单的方法。

谢谢Moz。我已经开始尝试使用TProcessMemoryCounters,但是这个只提供工作集内存,也就是RAM内存,对吧?我猜测,如果出现“内存不足”错误,那么是总的虚拟内存太大了吗?如果我错了,请纠正我。我会尝试使用/3G开关。 - Bourgui
@Bourgui,“内存不足”是一个非常广泛的信息,现代计算机应该拥有(更多)超过2GB的物理内存,所以你不应该用尽虚拟内存。ProcessMemoryCounters结构中还有其他信息,我建议记录整个结构并查看其变化。记录日志也将帮助您确定出错的位置。我建议还要查看异常的堆栈跟踪。在最坏的情况下,如果发现日志记录失败,则可能需要编写外部记录器。但在需要之前请避免这样做。 - Мסž
感谢您的反馈。这正是我所想的。不幸的是,我的大部分堆栈跟踪都没有带我到任何地方,而且大多数情况下都不一致。自那以后,我已经成功地隔离了一个问题(请参见我的 OP 编辑)-希望它是我所有问题的根源。 - Bourgui

2

由于Delphi受限于32位地址空间,这些问题变得越来越普遍。

你可以采取的第一件最简单的事情是在64位操作系统上运行,并将可用地址空间从32位操作系统上的2GB移动到4GB地址。这不会自动发生,你需要将应用程序标记为LARGEADDRESSAWARE。通过在.dpr文件中添加以下内容来实现:

const
  IMAGE_FILE_LARGE_ADDRESS_AWARE = $0020;

{$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE}

另一个常见的内存错误原因不是内存短缺,而是您正在请求一个大的连续内存块,而没有单个连续地址空间块可用。
解决这个问题更加困难。首先,您需要确定当前需要大量连续内存块的代码部分。接下来,您必须修改正在进行此操作的对象,并安排它们请求小块内存,然后将它们“拼接”在一起以形成较大的块的外观。根据我的经验,这通常发生在使用动态数组的代码中。

谢谢David。问题:使用FastMM时,那个和/3G标志有什么区别?正如我在编辑中提到的,我发现了一个可能是我的麻烦源头的问题,由于大多数内存分配相对较小(它们不超过10 MB),而且我们的应用程序正在运行在没有被用于其他任何事情的系统上,我怀疑(并希望!)这里不是原因。更不用说我尽可能地避免使用动态数组。 - Bourgui
@Bourgui 几十兆的文件,重复可能会有问题。/3G 是32位版本。你仍然需要将你的exe标记为LARGEADDRESSAWARE。然后你需要使用/3GB启动Windows。有人这样做吗?无论如何,你的客户服务器肯定是64位机器。你是否将你的应用程序标记为LARGEADDRESSAWARE?如果没有,为什么不呢? - David Heffernan
这样的大内存分配在应用程序中相对较少,而且是时间持久的。这些分配也有很好的日志记录,我们应该能够相对容易地注意到这些问题。这就是为什么我认为那不是问题所在的原因。但我还没有完全排除任何可能性。事实上,我们从来没有遇到过内存问题,所以我们从来没有动力使我们的应用程序具有LargeAddressAware功能,但这绝对是值得研究的——我不能一直为Delphi 64位编译器屏住呼吸!;o) - Bourgui
嗯,听起来你的代码可能存在错误,而不是真正的内存问题。 - David Heffernan

2
你能在出现错误时展示一下堆栈跟踪吗?错误发生时它消耗了多少内存?
我之前制作了一个内存记录器(用于FastMM),可以记录两个点之间的所有内存(使用“startlog”和“endlog”过程),以查找“软泄漏”:我在列表中分配了对象,但从未清除该列表,只有在关闭应用程序时才清除,因此FastMM报告没有泄漏。通过使用我的内存记录器,我可以找到这些对象(它仅记录在执行“endlog”过程之前未释放的新分配的内存)。 我会看看是否能找到这段代码。
顺便说一句:您还可以通过以下三种方式之一获得“内存不足”:
- FastMM在分配时检测到错误时会给出此错误。因此并非真正的“内存不足”,而是由于损坏等原因引起的内部fastmm错误。 - 分配一个非常大的块(例如1GB)。我曾经因为流读取错误(RemObjects)而获得了这个错误,所以它读取了一个字符串的错误大小值,因此尝试预先分配一个(随机的)大字符串。这样的错误看起来很奇怪,因为在我的情况下,我的应用程序已经分配了约150MB,因此也不是真正的“内存不足”。 - 碎片化:如果您尝试分配10MB的块,但Windows找不到一个连续的10MB块,则Windows会给出“内存不足”错误。
因此,请在出现错误时提供堆栈跟踪和使用的内存量!

嗨,André,谢谢你的回复。我没有发布堆栈跟踪,因为它们都不同。然而,我们可能已经找到了问题,请看我的编辑。 - Bourgui

1
当我遇到“内存不足”错误时,通常是由于一个无限循环。这个循环通常会分配内存,并且在所有可用内存被使用之前不会停止。释放内存不是问题,因为程序从未到达那个点。一些让我困扰的代码类型包括:
- “while not x.Eof do” 循环没有“x.Next”来遍历数据集,或者 - 递归过程或子例程从未遇到退出条件。
我会寻找任何可能在某些情况下继续“永远”运行的循环或递归,例如在内存中构建大量数据结构。

谢谢crefird,但我不认为这是情况。请看我的编辑,了解我们迄今为止发现的更多信息。 - Bourgui

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