
简介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 默认管理器FastMM4FastMM5接入成本无放第一个 uses 即可需要适配新接口小对象性能一般好并发场景更好内存泄漏检测无完备部分越界 / double-free 检测无FullDebugMode 下有支持较弱64 位支持有有有成熟度高高相对较新从我自己的使用习惯来说凡是“要交付给客户、出问题必须能自证清白”的 Delphi 程序都用 FastMM4只有纯算法验证、无长期运行需求的小工具才会随手留在默认管理器上。选择 FastMM4 最重要的理由不是快而是 FullDebugMode 下它能在崩溃之前拦住错误。调优和排错通常是两码事FastMM4 把这两件事放在同一个包这是它最值得的位置。3. 在 Delphi 工程里接入 FastMM497.zip3.1 最小接入dpr 第一行还是 Project 设置FastMM497.zip 解开后里面核心就两个文件FastMM4.pas和FastMM4Options.inc。把这两个文件放到工程目录或者在 Tools Options Library path 里加进搜索路径然后在dpr文件的 uses 最前面加上 FastMM4编译即可。不需要改动任何现有代码也不需要引入额外的包或设计期组件。注意点有三个。第一uses 顺序不能错FastMM4 必须在第一个。第二如果用了运行时包Runtime PackagesFastMM 只在主程序里生效包内代码仍可能走包的引用通常不建议在开启 Runtime Packages 的场景下用 FastMM 的调试功能。第三对于 CBuilder 混编项目必须保证 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}分支下定义 FullDebugModeRelease 下无论 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 的泄漏优先统一编译器运行时才是正道。本文还有配套的精品资源点击获取