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

时间:2010-04-02 23:09:53

标签: delphi memory-management fastmm

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

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

使用spenwarr对How to get the Memory Used by a Delphi Program的回答中给出的CurrentMemoryUsage例程,我已经显示了FastMM4在处理过程中使用的内存。

似乎正在发生的事情是,在每个进程和发布周期之后,内存的使用正在增长。 e.g:

在没有数据集的情况下启动程序后使用了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和梅森的回答。你让我思考并尝试让我意识到自己错过了什么的事情。因此需要进行详细的调试。

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

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

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

4 个答案:

答案 0 :(得分:25)

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

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

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

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

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

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

在涉及兆字节小内存分配的大量内存密集型操作中,堆碎片非常常见 - 特别是如果与内存密集型操作无关的内存的内存分配与大型作业同时进行。例如,如果通过80MB数据库操作进行运算,也会随着进度的推移将状态输出到列表框,则用于报告状态的字符串将分散在数据库内存块中的堆中。当你释放数据库计算所使用的所有内存块时,列表框字符串仍在那里(在使用中,没有丢失),但它们遍布整个地方,可能占用每个小字符串的整个操作系统大块。

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

答案 1 :(得分:1)

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

我假设您的数据集位于表单上,并且在表单清除其组件时会自动释放它。尝试将MyDataset := nil;放入表单的OnDestroy中。这将确保数据集泄漏,并且数据集拥有的所有内容。在加载两次之后一次又一次地加载并尝试比较泄漏报告,并查看是否为您提供了有用的东西。< / p>

答案 2 :(得分:0)

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

详情请见:My program never releases the memory back. Why?

答案 3 :(得分:0)

您可以使用VMMap来跟踪分配最多的字节。对于类似的情况,它也有帮助。

  • 下载VMMap
  • 使用map file detailed
  • 编译应用程序
  • 将映射文件转换为dbg,让VMMap理解它。使用map2dbg工具
  • 在VMMap上配置符号(dbg)路径:选项->配置符号->符号路径
  • 在VMMap上配置源路径->选项->配置符号->源代码路径。提示:使用“ *”包括子文件夹
  • 在VMMap中,转到“文件”->“选择进程”->“启动并跟踪新进程”。配置应用程序及其所需的任何参数。那好吧。

打开应用程序时,VMMap将使用绕行分配/释放方法来跟踪所有分配和释放的内存。您可以在“时间轴”按钮(位于VMMap的底部)上看到内存的时间轴(很明显)。

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

示例应用程序:

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中还有许多其他功能可以帮助解决内存问题。