简介:FastMM4 是一款面向 Delphi 开发者的开源内存管理库,本压缩包即其 4.97 版本完整资源,适用于需要排查内存泄漏、双重释放、访问越界等问题的中高级 Pascal 开发者。包内共 89 个文件,以 FastMM4.pas 等 29 个 Pascal 源文件为核心,辅以 FastMM4Options.inc 配置模板、FastMM4Messages.pas 多语言消息文件,以及为 C++ Builder 准备的 .cpp/.def 文件和预编译 DLL,覆盖 FullDebugMode、Usage Tracker 等调试模式;压缩包仅 799KB,轻量易集成。目前已有 270 人学习下载。资源内附 FastMM4_Readme.txt 与 FastMM4_FAQ.txt 说明文档,并包含 Demos 示例目录,可帮助读者快速掌握替换 BorlndMM DLL、配置内存检测选项及分析错误报告的完整流程,是提升 Delphi 程序稳定性与内存管理效率的实用工具。
1. 拿到 FastMM497.zip 先别急着解压,Delphi 内存管理这件事值得重新捋一遍
一个 Delphi 服务端程序跑上几天后内存稳步上涨,或者一个桌面工具在释放窗体时偶尔崩溃、报错位置却飘在完全无关的第三方控件里。这种问题新手第一反应是“我代码写错了”,老手会先把内存管理器换掉再谈查错。FastMM497.zip 里装的 FastMM4,就是 Delphi 生态里最常用、也最值得先装上去的内存管理器实现。它解决的问题分两层:一是替代 RTL 默认 Manager 后让多线程分配更稳、碎片更少,二是打开 FullDebugMode 后能在退出时列出泄漏点、越界写和 double-free。适合谁?写 Delphi 7 到 Delphi 12、32 位或 64 位程序,觉得自己对内存没把握的人。安装它不要求懂底层内存分配,但理解它背后池化和检测机制,才能把配置文件里的开关用对。
2. FastMM 内存管理器的结构与选型取舍
2.1 FastMM 替换的是 Delphi RTL 的什么机制
Delphi 里GetMem、FreeMem、ReallocMem以及New/Dispose、字符串和动态数组的自动管理,最终都走一个全局的TMemoryManager记录体。RTL 启动时默认安装的是系统堆的薄封装,它快,但不会给你任何调试信息。FastMM4 做的事就是自己维护一套内存池,在程序最早期把自己注册进TMemoryManager,让所有分配和释放都经过它。
这个替换发生的时间点决定了一切。如果FastMM4.pas不是dpr文件 uses 列表里的第一个单元,RTL 可能已经把默认管理器装好了,虽然 FastMM 仍然可以覆盖它,但早期发生在 initialization 里的分配动作可能已经落到了旧的堆上。最稳妥的做法是让 FastMM4 成为第一个单元:
program LeakDemo; uses FastMM4, // 必须第一个,确保内存管理器最先就位 System.SysUtils, Classes; {$R *.res} type PData = ^TData; TData = record Id: Integer; Name: string; end; var Data: PData; begin GetMem(Data, SizeOf(TData)); // 这里有意识泄漏一次 Data.Id := 1; Writeln('press any key...'); Readln; end.这段代码故意不调用FreeMem。编译运行后退出时,如果配置了泄漏报告,FastMM 会弹窗或写日志,指出第几个调用栈产生了泄漏。逻辑上值得注意的一点是GetMem拿到的是未初始化的旧内存,PData里有个string字段,不先初始化就赋值会造成严重的双重释放问题,所以实际写业务代码要遵守“所有复杂字段先Initialize或直接用New”。FastMM497.zip 自带的示例代码里几乎都遵循这一规则,一个良好的分配习惯是New(Data)、结束时Dispose(Data),能自动处理 string 和 interface 的初始化和清理。
2.2 内存池化与线程局部堆
FastMM 的核心设计是“小对象不进系统堆”。它把分配分成 Small、Medium、Large 三类,Small 块固定大小(从 16、24、32 字节到若干 KB),按大小分箱管理。每个箱子维护空闲链,释放回链表中,同类块复用,减少向操作系统申请内存的次数。这样做的效果是:频繁GetMem/FreeMem同一类大小的对象时,几乎不产生系统调用,也不会在堆上留下碎片空洞。
多线程场景下,FastMM 会给每个线程缓存一小部分空闲块,释放时优先返还到当前线程的缓存中。这样另一个线程尽量不碰同一组空闲链,锁冲突降低。这是它相对 RTL 默认管理器最明显的性能优势。当线程数超过 16 个、分配频率高时,差别可以从几个百分点拉到几十个百分点,具体取决于对象大小分布。
2.3 FastMM4 与 FastMM5、其他内存池怎么选
网上讨论内存池时经常把 FastMM4 和 FastMM5 混在一起。FastMM4 是今天标题里这个发行包,稳定、文档多、调试能力完整,适合绝大多数业务系统;FastMM5 是后续重新设计的版本,接口不兼容,主要优化点在无锁化,但周边工具和踩坑案例少。选型时可以参考这个表:
| 对比项 | RTL 默认管理器 | FastMM4 | FastMM5 |
|---|---|---|---|
| 接入成本 | 无 | 放第一个 uses 即可 | 需要适配新接口 |
| 小对象性能 | 一般 | 好 | 并发场景更好 |
| 内存泄漏检测 | 无 | 完备 | 部分 |
| 越界 / double-free 检测 | 无 | FullDebugMode 下有 | 支持较弱 |
| 64 位支持 | 有 | 有 | 有 |
| 成熟度 | 高 | 高 | 相对较新 |
从我自己的使用习惯来说,凡是“要交付给客户、出问题必须能自证清白”的 Delphi 程序,都用 FastMM4;只有纯算法验证、无长期运行需求的小工具,才会随手留在默认管理器上。选择 FastMM4 最重要的理由不是快,而是 FullDebugMode 下它能在崩溃之前拦住错误。调优和排错通常是两码事,FastMM4 把这两件事放在同一个包,这是它最值得的位置。
3. 在 Delphi 工程里接入 FastMM497.zip
3.1 最小接入:dpr 第一行还是 Project 设置
FastMM497.zip 解开后里面核心就两个文件:FastMM4.pas和FastMM4Options.inc。把这两个文件放到工程目录,或者在 Tools > Options > Library path 里加进搜索路径,然后在dpr文件的 uses 最前面加上 FastMM4,编译即可。不需要改动任何现有代码,也不需要引入额外的包或设计期组件。
注意点有三个。第一,uses 顺序不能错,FastMM4 必须在第一个。第二,如果用了运行时包(Runtime Packages),FastMM 只在主程序里生效,包内代码仍可能走包的引用;通常不建议在开启 Runtime Packages 的场景下用 FastMM 的调试功能。第三,对于 C++Builder 混编项目,必须保证 C++ 侧的new/delete最终走 Delphi 的分配器,否则跨语言释放会崩溃。常见做法是把 FastMM4 放在最前,并保证FreeMem与GetMem在同一个模块内配对。
3.2 FastMM4Options.inc 关键配置项
安装完不等于能干活。FastMM 的调试能力集中在一个被 include 的配置文件中。FastMM4Options.inc里全是默认关闭的开关,按需打开。常用的几个我一般这样配置:
{$define FullDebugMode} // 开启完整调试,拦截越界、重复释放 {$define EnableMemoryLeakReporting} // 程序退出时报告泄漏 {$define LogErrorsToFile} // 错误写入日志文件,而不是只弹窗 {$define RawStackTraces} // 采集原始调用栈FullDebugMode一打开,FastMM 会在每个分配块前后加上额外的 guard 区域(红区),并延迟释放被 FreeMem 的块,目的是让“释放后再使用”和“越界写”可以被抓到。它也会显著增加内存占用、拖慢程序运行,所以只应该在 Debug 配置下打开。EnableMemoryLeakReporting的含义很直接:FastMM 在程序退出时遍历所有未释放块,报告泄漏次数和调用栈。LogErrorsToFile则决定错误去向,不打开时 FastMM 用弹窗展示,程序挂在 CI 机上没人点确定就停在那了;打开后运行目录下会生成日志文件,把错误按时间追加进去。
RawStackTraces比较特殊。只开FullDebugMode时报告里看不到调用栈,只能看到一个地址;打开StackTraces相关选项会用一个 walker 去回溯栈帧。这是一个性能与信息量的取舍:全程序开栈回溯能最直接定位泄漏点,但高频分配时开销不小。我的方案是先用日志跑短时间复现问题,定位到模块后再针对性地打开。
3.3 32 位与 64 位、Debug 与 Release 的配置差异
FastMM497.zip 时代,32 位编译器和 64 位编译器共用一份源码,但地图文件格式不同,栈回溯的还原精度也不一样。32 位下用turbo或jclDebug能把调用栈映射成函数名;64 位下如果没有做map2dbg转换,报告里的栈地址往往是偏移量,只能靠人工对照汇编。这不是 FastMM 的缺陷,是 PE 格式本身不内嵌符号表。接 Map 文件的实践是:Delphi 编译器生成.map,用微软的map2dbg转成.dbg后和 exe 放在一起,FastMM 报告出的调用栈才可读。
Release 配置下要处理另一个坑:很多人把FullDebugMode留在条编译指令里忘了关,交付出去的程序比应该的慢一截,还多了几个 GB 内存。标准做法是按配置区分:
{$ifdef Debug} {$define FullDebugMode} {$define LogErrorsToFile} {$endif}Debug 配置开启完整诊断,Release 配置保持默认的快速分配路径。泄漏报告这个功能在 Release 下也建议打开,因为它几乎没有运行时开销,只在退出时扫描一次,这对排查“用户环境里程序退出时才崩溃”的情况很有用。
4. 用 FastMM 定位内存泄漏与越界访问
4.1 让泄漏弹窗与日志文件出现的最小复现
搭建一个能复现泄漏的最小工程,比直接拿大项目试错要快得多。上面那个LeakDemo保存后,用 Debug 配置编译运行,输入任意字符后退出。如果配置正确,程序退出时会出现一个标题类似“Memory Leak”的对话框,内容会列出泄漏的块大小、调用栈和造成泄漏的函数。如果在dpr开头加了LogErrorsToFile,这份报告同时会写到运行目录下的日志文件里,方便事后查看。
注意:控制台程序的退出流程很短,Writeln之后直接结束,FastMM 的泄漏报告在 finalization 阶段执行,此时标准输出可能已经关了,所以别指望日志打印到控制台。想从 TViewer 看报告,可以查.log文件。文件名的具体形式不同版本略有差别,最简单的方式是跑完程序后看 exe 目录下按修改时间倒序的.log文件。
4.2 读懂 FastMM 报告:调用栈与泄漏类型
典型的报告长这样:
This block was allocated by thread 0x1B20, and is being leaked: 4 - 7 bytes: UnicodeString Leaked at: - TForm1.Button1Click(Line: 42) - TForm1.Button1Click(Line: 45)它告诉你三点:泄漏块大小、对象类型(如果能识别出 string、动态数组等)、分配时的调用栈。定位代码时最有用的是最后几行,因为它标记了实际分配位置。如果看到Leaked at下一行是类似$140004A的裸地址,说明符号还原没生效,或者调用栈被编译器优化截断。此时回到FastMM4Options.inc检查是否开了RawStackTraces并且关闭了 map 文件,或者确认StackTraces相关选项没有被注释掉。
报告里还有一类不是泄漏但很扎眼的内容:重复释放、释放后写、块头破坏。FastMM 检测到这些会直接中止程序并弹窗,桩信息会写明“Block modified”以及它认为的越界范围。遇到这类信息时,错误点往往在日志里标出的最近一次释放附近,但这只是信号的发出位置,真正的凶手可能是在别处写坏了内存。常见排查手段是关掉FullDebugMode跑一遍,如果程序不崩了,就证明错误真的是内存越界,而不是逻辑 bug。
4.3 FullDebugMode 下的常见坑与误用
第一个坑是误以为开了 FullDebugMode 就能自动识别所有泄漏。FastMM 能捕获的是GetMem/AllocMem/New且没有对应释放的块。如果你用一个第三方 C 库在 DLL 内部用malloc分配了内存,泄漏不归 FastMM 管,因为那个 DLL 有自己的 CRT 堆。处理这类混合内存问题要不跨边界释放,要不把 DLL 的 CRT 也重定向到 FastMM。
第二个坑是释放时序。FastMM 在退出时检查泄漏,但某些全局对象在 finalization 之后才释放,会报告为“伪泄漏”。典型例子是单例对象在initialization里创建、在finalization里释放,如果释放顺序排在 FastMM 的 finalization 之后,FastMM 就会咬住不放。解决办法是调整FastMM4Options.inc中的泄漏报告时序,或者显式地在finalization里先释放再让 FastMM 检查。
第三个坑是重名单元。有些人从旧项目里复制了另一个版本的FastMM4.pas到 Library path,路径顺序错误导致编进来的不是 FastMM497.zip 里那份,配置改了但行为不变。验证方法:在dpr里临时写一行Writeln(FastMMVersion)或者编译时查看生成的 map 文件确认单元路径。
5. 发布阶段与多线程下的 FastMM 调优
5.1 Release 构建关闭诊断,保留性能
诊断功能全开时程序会慢,这在 Debug 下不心疼,但发布版必须走另一条路径。我的惯例是在 Release 配置里保留泄漏报告开关,关掉 FullDebugMode,再关掉栈回溯,因为栈回溯影响的是单次分配速度,而 FullDebugMode 影响的是分配块的数量和生命周期。若在 Release 下需要快速验证服务的稳定性,额外一招是把LogErrorsToFile打开:程序退出时才写日志,运行期几乎没有 IO 开销,不会干扰性能测试数据。
以下是 Release 配置的实践写法,放在工程选项的 Conditionals defines 里:
Release := false; {$ifdef FullDebugMode} // 这里什么都不做,禁止 Release 误开 FullDebugMode {$endif}其实更干净的方式是在FastMM4Options.inc里直接判断编译器指令,不要让用户工程里的 defines 和 inc 里的重复配置打架。一种常见约定是 FastMM 自己只在{$IFDEF DEBUG}分支下定义 FullDebugMode,Release 下无论 inc 里写什么都被忽略,我建议自己在 inc 里加上同样的约束,避免交付时误带诊断模式。
5.2 多线程场景:检查锁竞争与内存碎片
FastMM 在多线程场景的收益来自线程局部空闲链表,但它能做的优化是有上限的。当分配块过大(超过 Small 箱范围),FastMM 会回落到系统堆或者自己的中型分配器,这里锁竞争仍然存在。排查这类瓶颈不能靠猜,要看生产环境的数据:运行任务管理器看 Private Bytes,或者用GetHeapStatus拿空闲块数量。
实际项目中一个有效做法是给服务程序做周期性快照:
uses System.SysUtils; var He: THeapStatus; begin He := GetHeapStatus; Writeln(Format('TotalAllocated: %d, FreeSmall: %d, FreeBig: %d', [He.TotalAllocated, He.FreeSmall, He.FreeBig])); end;上面的GetHeapStatus返回的是 TMemoryManager 当前注册的管理器自身统计,FastMM 接管后它反映的就不是系统堆,而是 FastMM 的池状态。观察TotalAllocated是否随业务推进持续增长且不回落,基本能分辨是泄漏还是合理缓存增长。FreeSmall不断增大则说明碎片或大对象频繁分配。这个指标只能定位方向,具体定位还是要回到第 4 章的报告。
5.3 和第三方库、外部内存管理器的边界
边界问题最容易在大型项目里翻车。如果多个 Delphi 包各自静态链接了不同的内存管理器副本,互相持有的内存由另一个管理器释放,轻则分配器崩溃,重则数据损坏。使用 FastMM 时有一个隐含前提:整个进程里只有一份注册过的TMemoryManager。RTTI 库、ORM、第三方 UI 控件都可能引用默认管理器,但只要它们的源码里不会强制SetMemoryManager,就不会冲突。遇到第三方提供的 DLL,更要避免在 DLL 内部释放宿主传入的内存,或者反过来。
一个快速验证 FastMM 是否全局生效的方法是调用系统 API 拿分配地址,对比是否落在 FastMM 的保留区间内。这条在 32 位下可用保留区基址判断,64 位下不如直接跑一次泄漏检测放心。检测到未生效时,优先查 dpr 顺序、项目文件里有没有重复 include、Library path 是否有不同版本的 FastMM4.pas。这些排查链路通常五分钟内能定位,比翻代码找谁偷换了管理器要快得多。
# 在 Windows 下用 Sysinternals 的 listdlls 检查加载的 DLL 是否包含其他 CRT listdlls -v app.exe | findstr /i "ucrt msvcr"如果输出里看到两套不同的 CRT,说明程序里很可能存在第二个内存分配体系,这时候 FastMM 的报告永远有盲区。不要试图用 FastMM 来解决跨 CRT 的泄漏,优先统一编译器运行时才是正道。
本文还有配套的精品资源,点击获取