(delphi11最新学习资料) Object Pascal 学习笔记---第13章第5节 (跟踪和检查内存 )

13.5 跟踪和检查内存

​ 在本章中,我们已经了解了Object Pascal 中内存管理的基础。在大多数情况下,只要应用这里强调的规则,就足以保持程序的稳定,避免过多的内存使用,基本上可以让你忘记内存管理。本章后面还将介绍一些编写稳健应用程序的最佳实践。

​ 在本节中,我将重点介绍用于跟踪内存使用、监控异常和查找内存泄漏的技术。这对于开发人员来说是非常重要的知识,即使它不是严格意义上的语言的一部分,而更多是运行时支持的一部分。此外,内存管理器的实现取决于目标平台和操作系统,你甚至可以在 Object Pascal 应用程序中插入一个自定义内存管理器(不过这并不常见)。

​ 请注意,所有与跟踪内存状态、内存管理器和泄漏检测相关的讨论都只涉及堆内存。栈和全局内存的管理方式不同,你基本上无权干预,但这些内存区域也很少引起任何问题。

13.5.1 内存状态

​ 如何跟踪堆内存状态呢?RTL 提供了几个方便的函数:GetMemoryManagerState 和 GetMemoryMap。内存管理器状态显示的是已分配的各种大小的块的数量,而堆地图则相当不错,因为它描述了应用程序在系统级的内存状态。

你可以通过编写以下代码来检查下面每个内存块的实际状态:

for I := Low(AMemoryMap) to High(AMemoryMap) do
begin
  case AMemoryMap[I] of
    csUnallocated: ...
    csAllocated: ...
    csReserved: ...
    csSysAllocated: ...
    csSysReserved: ...
  end;
end;

​ 这段代码在ShowMemory示例中用于创建应用程序内存状态的图形表示。

13.5.2 FastMM4

​ 在 Windows 平台上, Object Pascal 当前的内存管理器称为 FastMM4,它是一个开源项目,主要由 Pierre La Riche 开发。在其他平台上,Delphi 使用平台原生的内存管理器。

​ FastMM4 优化了内存分配,加快了分配速度,并释放出更多内存供后续使用。FastMM4 在高效的内存清理、对已删除对象的错误使用(包括基于接口的数据访问)、内存覆盖和缓冲区超限等方面能够进行大量的内存检查。它还能提供一些关于遗留对象的反馈信息,帮助你追踪内存泄漏。

​ 实际上,FastMM4 的某些更高级功能只有完整版库才提供(在 "完整 FastMM4 中的缓冲区超限 "一节中介绍),而不是标准 RTL 中包含的版本。因此,如果您想获得完整版的功能,就必须从以下地址下载完整源代码:
​ https://github.com/pleriche/FastMM4

注解:该库有一个名为 FastMM5 的新版本,专门针对多线程应用进行了优化,在大型多核系统上的性能要好得多。该库的新版本可通过 GPL 许可(适用于开源项目)或付费商业许可(物有所值)获得。欲了解更多信息,请访问 https://github.com/pleriche/FastMM5。

13.5.3 追踪泄漏和其他全局设置

​ FastMM4 的 RTL 版本可以通过系统单元中的全局设置进行调整。请注意,虽然相关的全局声明在系统单元中,但实际的内存管理器是在 getmem.inc RTL 源代码文件中实现的。

​ 最容易使用的设置是 ReportMemoryLeaksOnShutdown 全局变量,通过它可以轻松跟踪内存泄漏。你需要在程序开始执行时打开它。然后,当程序终止时,它会告诉你代码(或你正在使用的任何库)中是否存在内存泄漏。

注解 内存管理器的更多高级设置包括:用于多线程分配的 NeverSleepOnMMThreadContention 全局变量;函数 GetMinimumBlockAlignment 和 SetMinimumBlockAlignment,这两个函数可以加快某些 SSE 操作的速度,但代价是使用更多内存;以及通过调用全局过程 RegisterExpectedMemoryLeak 来注册预期内存泄漏的功能。

​ 为了演示标准的内存泄漏报告和注册,我编写了一个简单的 LeakTest 示例。它有一个带有 OnClick 处理程序的按钮:

var
P: Pointer;
begin
  GetMem(P, 100); // Memory leak!
end;

​ 这段代码分配了 100 个字节,这些字节丢失或泄漏了。如果你在集成开发环境中运行 LeakTest 程序,并按下第一个按钮一次;然后,当你关闭程序时,你会收到一条类似图 13.6 上半部分的信息。

​ 程序的另一个 "泄漏 "是由创建 TButton 并将其留在内存中造成的,但由于该对象包含许多子元素,泄漏报告变得更加复杂,如图 13.6 下部所示。尽管如此,我们还是掌握了一些关于泄漏本身的有限信息。

​ 图 13.6: LeakTest 程序终止时 Windows 内存管理器报告的内存泄露情况

​ 程序还为一个永远不会被释放的全局指针分配了一些内存,但通过按预期注册这个潜在的泄漏,它不会被报告:

procedure TFormLeakTest.FormCreate(Sender: TObject);
begin
  GetMem(GlobalPointer, 200);
  RegisterExpectedMemoryLeak(GlobalPointer);
end;

​ 同样,这种基本的泄漏报告功能仅适用于 Windows 平台,该平台默认使用 FastMM4。

13.5.4 FastMM4完整版中的缓冲区溢出

​ 这是一个相当高级的话题,而且特定于Windows平台,因此我建议只有最经验丰富的开发人员阅读这一节。

​ 如果要更精细地控制泄漏报告(例如激活基于文件的日志记录),微调分配策略并使用FastMM4提供的内存检查,您需要下载完整版本。这包括FastMM4.pas文件和FastMM4Options.inc配置文件。

​ 您可以通过编辑后者来微调设置,只需在 $DEFINE 语句前面加上一个句点,将其变为纯注释,如以下两行中的第一行所示:

{.$DEFINE Align16Bytes} // 注释
{$DEFINE UseCustomFixedSizeMoveRoutines} // 激活的设置

​ 在这个演示中,我打开了以下相关设置,在此报告是为了让你了解可用的定义类型:

{$DEFINE FullDebugMode}
{$DEFINE LogErrorsToFile}
{$DEFINE EnableMemoryLeakReporting}
{$DEFINE HideExpectedLeaksRegisteredByPointer}
{$DEFINE RequireDebuggerPresenceForLeakReporting}

​ 测试程序(在 FastMMCode 文件夹中,其中还包括我使用的 FastMM4 版本的完整源代码,以方便您使用)通过将其设置为第一个单元,激活了项目源代码文件中的自定义版本内存管理器:

program FastMMCode;
uses
  FastMM4 in 'FastMM4.pas',
  Forms,
  FastMMForm in 'FastMMForm.pas'; {Form1}

​ 你还需要一个 FastMM_FullDebugMode.dll 文件的本地拷贝来使其工作。由于 Length(Caption) 大于提供的 5 个字符,本地缓冲区无法容纳更多文本,因此该演示程序会导致缓冲区超限:

procedure TForm1.Button2Click(Sender: TObject);
var
  PCh1: PChar;
begin
  GetMem(PCh1, 5);
  GetWindowText(Handle, PCh1, Length(Caption));
  ShowMessage(PCh1);
  FreeMem(PCh1);
end;

​ 内存管理器会在每个内存块的开头和结尾分配具有特殊值的额外字节,并在释放每个内存块时检查这些值。这就是为什么在调用 FreeMem 时会出现错误。当你(在调试器中)按下按钮时,你会看到一条很长的错误信息,这条信息也会被记录到文件中:

FastMMCode_MemoryManager_EventLog.txt

​ 这是内存溢出错误的输出,其中包含分配和释放操作时的堆栈跟踪,当前堆栈跟踪以及内存转储(这里是部分):

FastMM在FreeMem操作期间检测到错误。块页脚已损坏。
块大小为: 5
分配此块时的堆栈跟踪(返回地址): 40305E [System][System.@GetMem]
44091A [Controls][Controls.TControl.Click]
44431B [Controls][Controls.TWinControl.WndProc]
42D959 [StdCtrls][StdCtrls.TButtonControl.WndProc] 44446C [Controls][Controls.DoControlMsg]
44431B [Controls][Controls.TWinControl.WndProc] 45498A [Forms][Forms.TCustomForm.WndProc]
443A43 [Controls][Controls.TWinControl.MainWndProc] 41F31A [Classes][Classes.StdWndProc]
76281A10 [GetMessageW]
此块当前用于类的对象: Unknown
分配号为: 381
先前释放此块时的堆栈跟踪(返回地址): 40307A [System][System.@FreeMem]
42DB8A [StdCtrls][StdCtrls.TButton.CreateWnd]
443863 [Controls][Controls.TWinControl.UpdateShowing]
...

​ 这并不是非常明显,但它提供的信息足以让你开始追查错误。

​ 请注意,如果不在内存管理器中进行这些设置,基本上就不会出现任何错误,程序也能继续运行—不过,如果缓冲区超限影响到存储其他内容的内存区域,可能会出现随机错误。在这种情况下,你可能会遇到一些奇怪的、很难追查的错误。

​ 例如,我曾经看到对象数据的初始部分被部分覆盖,该部分存储了类引用。通过这种内存损坏,类变得未定义,对其虚拟函数的每次调用都会严重崩溃——这很难与程序中完全不同区域的内存写入操作联系起来。

13.5.5 非Windows平台上的内存管理

​ 考虑到对象 Pascal 编译器中的内存管理方式,值得考虑在其他平台下确保一切尽在掌握的一些选项。在我们继续之前,必须注意的是,在非 Windows 平台上,Delphi 不使用 FastMM4 内存管理器,因此设置 ReportMemoryLeaksOnShutdown 全局标志以在程序关闭时检查内存泄漏是没有用的。还有一个原因是,在移动设备上通常没有关闭应用程序的方法,因为应用程序会一直保留在内存中,直到被用户或操作系统强制删除。

​ 在 macOS、iOS 和 Android 平台上,Object Pascal RTL 会直接调用本地 libc 库的 malloc 和 free 函数。在该平台上监控内存使用情况的一种方法是依赖外部平台工具。例如,在 iOS(和 macOS)上,你可以使用 Apple 的 Instruments 工具,这是一个完整的跟踪系统,可以监控物理设备上运行的应用程序的所有方面。

13.5.6 跟踪每个类的分配

​ 最后,Object Pascal 还有一种跟踪特定类而不是整个内存管理的方法。事实上,对象的内存分配是通过调用 NewInstance 虚类方法进行的,而内存清理则是通过 FreeInstance 虚方法完成的。这些都是虚拟方法,你可以对给定的类进行覆盖,以定制特定的内存分配策略。

​ 这样做的好处是,无论构造函数(因为可以有多个构造函数)和析构函数如何,都可以这样做,从而将内存跟踪代码与标准的对象初始化和最终化代码清晰地分离开来。虽然这是一个相当极端的角落情况(可能只有在一些大型内存结构中才值得这样做),但你可以覆盖这些方法来计算创建和销毁的给定类对象的数量,计算活动实例的数量,并在最后检查计数是否如预期归零。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值