12

我使用的是 Delphi 2009,它内置了 FastMM4 内存管理器。

我的程序读入并处理一个大型数据集。每当我清除数据集或退出程序时,所有内存都会正确释放。它根本没有内存泄漏。

使用 spenwarr 的回答中给出的 CurrentMemoryUsage 例程:如何获取 Delphi 程序使用的内存,我已经显示了 FastMM4 在处理过程中使用的内存。

似乎正在发生的事情是,在每个进程和释放周期之后,内存使用量都在增长。例如:

在没有数据集的情况下启动我的程序后使用了 1,456 KB。

加载大型数据集后使用了 218,455 KB。

完全清除数据集后的 71,994 KB。如果我此时退出(或我的示例中的任何一点),则不会报告内存泄漏。

再次加载相同的数据集后使用了 271,905 KB。

完全清除数据集后 125,443 KB。

再次加载相同的数据集后使用了 325,519 KB。

完全清除数据集后 179,059 KB。

再次加载相同的数据集后使用了 378,752 KB。

似乎我的程序的内存使用在每个加载/清除周期增加了大约 53,400 KB。任务管理器确认这确实发生了。

我听说 FastMM4 并不总是在释放对象时将所有程序内存释放回操作系统,以便在需要更多内存时保留一些内存。但是这种持续的增长让我很困扰。由于没有报告内存泄漏,我无法确定问题。

有谁知道为什么会发生这种情况,如果它是坏的,如果有什么我可以或应该做的吗?


感谢 dthorpe 和 Mason 的回答。你让我思考和尝试让我意识到我错过了什么的事情。所以需要详细的调试。

事实证明,我所有的结构在退出时都得到了适当的释放。但是在运行期间每个循环后的内存释放不是。它正在累积内存块,如果我的退出清理不正确,通常会导致在退出时可以检测到的泄漏 - 但确实如此。

我需要在周期之间清除一些 StringLists 和其他结构。我仍然不确定我的程序如何正确处理早期周期中仍然存在的额外数据,但确实如此。我可能会进一步研究。

这个问题已经回答了。谢谢你的帮助。

4

4 回答 4

25

您链接到的 CurrentMemoryUsage 实用程序会报告您的应用程序的工作集大小。工作集是映射到物理内存地址的虚拟内存地址空间的总页数。但是,其中一些或许多页面中存储的实际数据可能非常少。因此,工作集是您的进程使用多少内存的“上限”。它指示保留了多少地址空间以供使用,但它不指示实际提交了多少(实际驻留在物理内存中)或提交的页面中有多少实际正在由您的应用程序使用。

试试这个:在你看到你的工作集大小在几次测试运行后逐渐增加后,最小化你的应用程序的主窗口。您很可能会看到工作集大小显着下降。为什么?因为当您最小化丢弃未使用页面并将工作集缩小到最小的应用程序时,Windows 会执行 SetProcessWorkingSetSize(-1) 调用。当应用程序窗口大小正常时,操作系统不会执行此操作,因为过于频繁地减小工作集大小会强制从交换文件中重新加载数据,从而降低性能。

更详细地了解它:您的 Delphi 应用程序以相当小的块分配内存 - 这里是一个字符串,那里是一个类。程序的平均内存分配通常少于几百字节。在系统范围内有效地管理这样的小分配是很困难的,因此操作系统不会。它有效地管理大内存块,特别是在 4k 虚拟内存页面大小和 64k 虚拟内存地址范围最小大小时。

这给应用程序带来了一个问题:应用程序通常分配小块,但操作系统以相当大的块分配内存。该怎么办?答案:子分配。

Delphi 运行时库的内存管理器和 FastMM 替换内存管理器(以及地球上几乎所有其他语言或工具集的运行时库)都存在做一件事:将操作系统中的大内存块分割成较小的块,供操作系统使用。应用。跟踪所有小块的位置、它们有多大以及它们是否被“泄露”也需要一些内存——称为开销。

在大量内存分配/释放的情况下,可能会释放 99% 的分配内容,但进程的工作集大小仅缩小 50%。为什么?大多数情况下,这是由堆碎片引起的:一小块内存仍在使用中,其中一个大块是 Delphi 内存管理器从操作系统获得并在内部分配的。使用的内存的内部计数很小(例如 300 字节),但由于它阻止堆管理器将它所在的大块释放回操作系统,所以那个 300 字节的小块的工作集贡献更像是 4k(或64k 取决于它是虚拟页面还是虚拟地址空间 - 我不记得了)。

在涉及兆字节小内存分配的大量内存密集型操作中,堆碎片非常常见 - 特别是如果与内存密集型操作无关的内存分配与大型作业同时进行时。例如,如果在处理 80MB 数据库操作时还会将状态输出到列表框,则用于报告状态的字符串将分散在数据库内存块的堆中。当您释放数据库计算使用的所有内存块时,列表框字符串仍然存在(正在使用,没有丢失),但它们分散在各处,每个小字符串可能占用整个 OS 大块。

尝试最小化窗口技巧,看看这是否会减少您的工作集。如果是这样,您可以忽略工作集计数器返回的数字的明显“严重性”。您还可以在大型计算操作之后添加对 SetProcessWorkingSetSize 的调用,以清除不再使用的页面。

于 2010-04-02T23:51:22.740 回答
1

您使用的是哪种数据集?如果它完全在 Delphi 中实现,(而不是用另一个内存管理器调用其他代码,比如 Midas,)你可以尝试故意泄漏数据集。

我假设您的数据集位于表单上,并且当表单清除其组件时它会自动释放。尝试放入MyDataset := nil;表单的 OnDestroy。这将确保数据集泄漏,以及数据集拥有的所有内容。 在加载两次后再次加载并比较泄漏报告后尝试,看看这是否给你任何有用的东西。

于 2010-04-02T23:55:35.873 回答
0

你是半泄漏的记忆;明显地。您在程序运行时泄漏内存,但是当您关闭程序时,您的数据集被正确释放,因此 FastMM(正确地)不会报告它。

有关详细信息,请参阅此内容:我的程序从不释放内存。为什么?

于 2010-12-19T17:53:49.057 回答
0

您可以使用 VMMap 来跟踪分配最多的字节。它帮助我解决了类似的情况。

  • 下载VMMap
  • 使用详细的地图文件编译您的应用程序
  • 将map文件转换成dbg,这样VMMap就可以理解了。使用map2dbg工具
  • 在 VMMap 上配置符号 (dbg) 路径:选项 -> 配置符号 -> 符号路径
  • 在 VMMap 上配置源路径 -> 选项 -> 配置符号 -> 源代码路径。提示:使用“*”来包含子文件夹
  • 在 VMMap 中,转到文件 -> 选择进程 -> 启动并跟踪一个新进程。配置应用程序和它需要的任何参数。然后确定。

当应用程序打开时,VMMap 将使用 allocate/free 方法中的弯路跟踪所有分配和释放的内存。您可以在 Timeline 按钮(在 VMMap 的底部)中看到内存的时间线(很明显)。

单击跟踪按钮。它将显示跟踪时间内的所有分配/解除分配操作。订购 Bytes 列以首先显示最多的字节,然后双击它。它将显示分配的调用堆栈。就我而言,第一项显示了我的问题。

示例应用程序:

private
  FList: TObjectList<TStringList>;
  ...
procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to 1000000 do
    FList.Add(TStringList.Create);
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  a: TStringList;
begin
  FList := TObjectList<TStringList>.Create; //not leak
  a := TStringList.Create; //leak
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FList.Free;
end;

单击一次按钮时,会看到 VMMap 中的 Trace,显示:

追踪表格

和调用堆栈:

调用栈

在这种情况下没有显示确切的代码,但 Vcl.Controls.TControl.Click 给出了一个想法。在我的真实场景中,帮助更多。

VMMap 中还有许多其他功能可以帮助分析内存问题。

于 2020-11-16T20:39:26.287 回答