我的Delphi程序为什么内存持续增长?

12

我正在使用内置FastMM4内存管理器的Delphi 2009。

我的程序读取并处理大型数据集。每当我清除数据集或退出程序时,所有内存都能正确释放,没有任何内存泄漏。

使用spenwarr在如何获取Delphi程序使用的内存中提供的CurrentMemoryUsage例程,我已经显示了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和其他结构需要清除。我仍然不确定我的程序如何在早期周期中仍然有额外数据的情况下正确工作,但它确实成功了。我可能会进一步研究这个问题。

这个问题已经得到解答。感谢你的帮助。


我个人认为,你应该将你的回答添加并接受,而不是编辑问题。这样做可以更方便别人查找问题、回答,并从你的经验中学习。 - EMBarbosa
4个回答

25
你提供的CurrentMemoryUsage实用程序报告了你应用程序的工作集大小。 工作集是映射到物理内存地址的虚拟内存地址空间页面的总数。 然而,其中一些或许很多页面实际上存储的数据非常少。 因此,工作集是进程使用内存的“上限”。 它表示保留供使用的地址空间有多少,但它不表示实际提交了多少(实际驻留在物理内存中)或提交的页面有多少被应用程序实际使用。
尝试一下:在几次测试运行后看到您的工作集大小缓慢增加后,最小化应用程序的主窗口。 您很可能会看到工作集大小显着下降。为什么?因为当您最小化一个应用程序时,Windows执行SetProcessWorkingSetSize(-1)调用,该调用会丢弃未使用的页面并将工作集缩小到最小值。该操作系统不会在应用程序窗口处于正常大小时执行此操作,因为过于频繁地减小工作集大小可能通过强制从交换文件重新加载数据而使性能变差。
更详细地了解它:Delphi应用程序分配内存的块相当小 - 这里是一个字符串,那里是一个类。程序的平均内存分配通常少于几百字节。在系统范围内高效地管理此类小分配很困难,因此操作系统不会这样做。它有效地管理大内存块,特别是在4k虚拟内存页面大小和64k虚拟内存地址范围最小大小方面。
这为应用程序提供了一个问题:应用程序通常分配小块,但操作系统会分配相当大的内存块。怎么办?答案:子分配。
Delphi运行时库的内存管理器、FastMM替换内存管理器(以及全球几乎所有其他语言或工具集的运行时库)都存在于一件事情:从操作系统中切割大内存块到应用程序使用的小块。跟踪所有小块的位置、大小和是否“泄漏”还需要一些内存,称为开销。在大量内存分配/释放的情况下,可能会出现这样一种情况:您释放了99%的分配内存,但进程的工作集大小仅缩小了50%。为什么?最常见的原因是堆碎片:一个小的内存块仍然在使用中,在Delphi内存管理器从操作系统获取并内部分配的大块中的一个中。虽然内存使用的计数很小(例如300字节),但由于它阻止堆管理器将其所在的大块释放回操作系统,因此那个小的300字节块的工作集贡献更像是4k(或64k,具体取决于是否是虚拟页面或虚拟地址空间-我记不清了)。
在涉及到多兆字节的小内存分配的大量内存密集型操作中,堆碎片非常常见,尤其是如果与大任务同时进行与内存分配无关的事物。例如,如果处理80MB的数据库操作时也输出状态到列表框,则用于报告状态的字符串将在堆中散布在数据库内存块之间。当您释放数据库计算使用的所有内存块时,列表框字符串仍然存在(在使用中,未丢失),但它们散布在各个位置,可能占用每个小字符串的整个操作系统大块。
尝试最小化窗口的技巧,以查看是否减少了工作集。如果确实如此,则可以忽略工作集计数器返回的表面“严重程度”。您还可以在完成大型计算操作后添加一个SetProcessWorkingSetSize调用来清除不再使用的页面。

他不应该看到堆碎片化导致的内存损失那么多。FastMM专门设计用于将碎片化降至最低。 - Mason Wheeler
5
我知道FastMM的设计。我曾帮助审查其实现并将其引入Delphi产品中。FastMM仍然容易出现夸大的工作集大小问题。 - dthorpe
谢谢,但最小化的效果很小,没有解决问题。我还尝试了Gabr在https://dev59.com/p3I-5IYBdhLWcg3wED9V#2033393上的内存清理例程,也没有帮助。顺便说一下,那里的Barry的答案非常有启发性。 - lkessler

1
你使用的是什么类型的数据集?如果它完全在Delphi中实现(不调用其他具有另一个内存管理器的代码,如Midas),您可以尝试故意泄漏数据集。
我假设您的数据集在一个窗体上,并且在窗体清除其组件时自动释放。尝试在窗体的OnDestroy中放置MyDataset := nil;。这将确保数据集泄漏,并且还会泄漏数据集拥有的所有内容。在加载一次后再次加载并比较泄漏报告,看看是否有用。

@Mason:也许我的术语“数据集”有误导性。通过“数据集”,我指的是一个基于文本或Unicode的大型输入文件,我将其作为FileStream读取,以及我从其中创建的内部数据结构。我没有使用数据库。我的程序是单个EXE。我不调用任何外部DLL。由于失去的内存块非常大,我怀疑它与那个文件流以及我正在加载的缓冲区有关。但是,EurekaLog或AQTime也没有报告任何泄漏。我现在进行了大量的调试工作。 - lkessler

0

你的程序存在内存泄漏问题,显然如此。当程序运行时,你正在泄漏内存,但是当你关闭程序时,你的数据集被正确释放,因此FastMM(理所当然地)不会报告它。

详见:我的程序为什么永远不会释放内存?


0

你可以使用VMMap来跟踪分配的最大字节数。这对我处理类似的情况很有帮助。

  • 下载VMMap
  • 使用map file detailed编译你的应用程序
  • 将map文件转换为dbg格式,以便VMMap能够理解。使用map2dbg工具
  • 在VMMap上配置符号(dbg)路径:选项->配置符号->符号路径
  • 在VMMap上配置源代码路径:选项->配置符号->源代码路径。提示:使用“*”包括子文件夹
  • 在VMMap中,转到文件->选择进程->启动和跟踪新进程。配置应用程序及其所需的任何参数。然后确定。

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

点击Trace按钮。它将显示跟踪时间内的所有分配/解除分配操作。将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 form

还有调用堆栈:

Callstack

在这种情况下没有显示具体的代码,但是 Vcl.Controls.TControl.Click 给了一个思路。在我的真实场景中,帮助更多。
VMMap 中有很多其他功能可以帮助分析内存问题。

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