ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FastMM4 实战:Delphi 内存泄漏排查与接入配置指南

FastMM4 实战:Delphi 内存泄漏排查与接入配置指南 简介FastMM4 4.97 是面向 Delphi 与 C Builder 开发者的开源内存管理库可直接替换编译器自带的内存管理器用于排查内存泄漏、双重释放、越界访问等棘手问题。压缩包共 89 个文件约 799KB以 pas 源码、dll 预编译库、dpr/dproj 工程文件、dfm 窗体、res 资源及 txt 说明文档为主另含 cpp、def、inc、bpr 等跨版本支持文件覆盖 Delphi 与 BCB 多版本目录结构。资源内含完整源码、预编译 FullDebugMode 组件、多语言消息文件与配置选项说明读者可据此集成到现有工程、按需调整内存分配策略与日志级别并借助详细错误报告定位泄漏点。目前已有 275 人学习下载适合希望提升代码稳定性、减少内存故障的 Pascal 开发者参考。1. 为什么我至今还在用 FastMM4 排查 Delphi 内存泄漏接手一个跑了七八年的 Delphi 老项目客户反馈“跑一晚上就卡死”任务管理器里内存曲线一路往上爬从不回落。这种场景下FastMM4几乎是绕不开的工具。它是一套开源内存管理库版本 4.97能直接替换 Delphi 自带的内存管理器把每一次分配、释放、越界、双重释放都记录下来。和市面上那些只报“泄漏了多少字节”的工具不同FastMM4 会告诉你泄漏块是在哪个单元、哪一行分配的甚至能给出调用栈。它适合两类人一是维护老 Delphi 项目、被内存问题反复折磨的工程师二是用 C Builder 但同样跑在 BorlndMM 上的开发者。这份资源包里除了核心的FastMM4.pas还带了 FullDebugMode 的预编译 DLL、多语言消息文件、C Builder 支持文件和一整套 Demo基本是拿来就能接进项目的状态。2. FastMM4 的替换原理与最小接入路径2.1 它到底替换了什么Delphi 程序默认走的是 BorlndMM.dll 里的内存管理器所有GetMem、FreeMem、对象构造析构最终都落到它头上。FastMM4 的做法是把这个入口整个接管过来在FastMM4.pas里重新实现GetMem、FreeMem、ReallocMem并在initialization段把自己注册成内存管理器。这样你不需要改业务代码只要保证FastMM4是项目里第一个被引用的单元它就能在所有其他单元之前完成接管。这里有个关键点单元引用顺序决定成败。Delphi 的初始化顺序是按uses列表从后往前执行 initialization所以FastMM4必须放在uses的第一个位置才能保证它在其他单元申请内存之前就完成替换。很多人接进去发现没效果十有八九是顺序放错了。2.2 三种接入方式怎么选资源包里其实提供了三条路径适用场景不同接入方式适用场景需要改动的文件源码直接引用常规 Delphi 项目需要完整调试能力在 dpr 的 uses 首位加FastMM4替换 BorlndMM.dll不方便改源码或 C Builder 项目用包里的 Replacement DLL 覆盖原文件FullDebugMode DLL需要详细报告但不想让主程序变大配合FastMM_FullDebugMode.dll使用常规做法是第一种。C Builder 项目因为编译模型不同通常走第二种或第三种资源包里的FastMM4BCB.cpp就是给 BCB5、BCB6、CB2006、CB2007 用的桥接文件。2.3 最小可复现的接入步骤先做一件事把FastMM4.pas、FastMM4Messages.pas、FastMM4Options.inc三个文件复制到项目目录。然后打开 dpr 文件把FastMM4放到 uses 最前面program MyProject; uses FastMM4, // 必须是第一个负责接管内存管理器 Forms, MainUnit in MainUnit.pas {Form1}; {$R *.res} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.这段代码的逻辑很直接FastMM4在 initialization 阶段完成内存管理器替换之后Forms、MainUnit里所有的内存操作都被它监控。参数上唯一要动的是FastMM4Options.inc后面单独讲。编译时如果报FastMM4Messages找不到检查它是否和FastMM4.pas在同一目录或者把搜索路径指过去。2.4 验证是否真的接管成功接进去不等于生效。最稳的验证办法是在FastMM4Options.inc里打开FullDebugMode然后故意写一段泄漏代码procedure TForm1.Button1Click(Sender: TObject); var P: Pointer; begin GetMem(P, 1024); // 故意不 FreeMem制造泄漏 end;程序退出时如果 FastMM4 正常工作会弹出一个FastMM4 Message对话框列出泄漏块的大小、地址和分配位置。如果什么都没弹先确认FastMM4Options.inc里FullDebugMode和EnableMemoryLeakReporting都是打开的再确认 dpr 里FastMM4确实排在第一位。这一步跑通后面才有意义。3. FastMM4Options.inc 里真正要动的几个开关3.1 这个文件为什么不能乱改FastMM4Options.inc是 FastMM4 的行为总控里面几十个{$define}和{$undef}。全开会让程序慢到没法用全关又等于白接。我的习惯是开发期开调试相关发布期只留必要的保护。资源包里自带的这个文件是默认配置需要按项目阶段手动调。3.2 开发期必开的三个开关{ FastMM4Options.inc 开发期配置片段 } {$define FullDebugMode} // 启用完整调试模式记录调用栈 {$define EnableMemoryLeakReporting} // 退出时报告泄漏 {$define ClearLogFileOnStartup} // 每次启动清空日志避免旧数据干扰 {$define LogErrorsToFile} // 错误写入文件方便事后分析FullDebugMode是核心它让 FastMM4 在每次分配时额外记录调用栈和分配序号代价是内存占用和速度都会明显下降所以只在开发期开。EnableMemoryLeakReporting控制退出时是否弹报告。ClearLogFileOnStartup很实用否则日志文件会越滚越大翻起来全是历史记录。LogErrorsToFile配合FullDebugMode用把报告写到FastMM4_Log.txt适合无人值守的测试机。3.3 发布期该关什么、留什么发布版本里FullDebugMode必须关掉否则性能损失用户能感知到。但有几个轻量开关建议保留{ FastMM4Options.inc 发布期配置片段 } {$undef FullDebugMode} // 关闭完整调试恢复性能 {$define EnableMemoryLeakReporting} // 保留泄漏报告但只报摘要 {$define AlwaysAllocateTopDown} // 减少地址空间碎片AlwaysAllocateTopDown让分配从高地址往下走能降低碎片率对长时间运行的服务端程序有帮助。EnableMemoryLeakReporting在发布期保留是因为它只统计不记录调用栈开销小万一线上出问题还能拿到泄漏摘要。3.4 日志文件与消息语言资源包里带了意大利语、葡萄牙语、南非荷兰语、法语、波兰语、德语、俄语、巴西葡萄牙语、印尼语、捷克语、乌克兰语、简体中文、白俄罗斯语、西班牙语、罗马尼亚语、英语的消息文件。默认是英语如果要中文报告把对应的中文消息文件内容合并进FastMM4Messages.pas或者直接替换其中的字符串常量。日志默认写到程序目录下的FastMM4_Log.txt如果程序装在Program Files下没有写权限需要改LogFileLocation指向可写目录否则报告会静默丢失——这个坑我踩过排查了半天才发现是权限问题。4. FullDebugMode 与 Replacement DLL 的实战用法4.1 FullDebugMode 的两种形态FullDebugMode 有两种跑法一种是编译进主程序的FastMM4.pas直接开FullDebugMode另一种是用资源包里的FastMM_FullDebugMode.dll。区别在于前者把调试逻辑编进 exeexe 会变大、启动变慢后者把调试逻辑放在独立 DLL 里主程序只保留一个轻量接口适合不想让发布包体积膨胀但又需要详细报告的场景。用 DLL 形态时需要把FastMM_FullDebugMode.dll放到 exe 同目录并在FastMM4Options.inc里定义UseFullDebugModeDLL。资源包里的FastMM_FullDebugMode.dproj和.dpr就是用来重新编译这个 DLL 的工程文件如果预编译的 DLL 和你的 Delphi 版本不匹配可以用它自己编一个。4.2 Replacement BorlndMM DLL 怎么用C Builder 项目或者不方便改源码的 Delphi 项目走替换 DLL 这条路。资源包里的Replacement borlndmm DLL目录下有针对不同版本编译好的borlndmm.dll。操作步骤# 1. 备份原始 DLL copy C:\Program Files (x86)\Embarcadero\Studio\xx.x\bin\borlndmm.dll borlndmm.dll.bak # 2. 用资源包里的替换版本覆盖 copy Replacement borlndmm DLL\borlndmm.dll C:\Program Files (x86)\Embarcadero\Studio\xx.x\bin\ # 3. 把 FastMM_FullDebugMode.dll 放到项目输出目录 copy FastMM_FullDebugMode.dll .\Win32\Debug\第一步备份是后悔药替换错了还能还原。第二步覆盖后所有用这个 IDE 编译出来的程序都会走 FastMM4 的内存管理器。第三步的 DLL 是 FullDebugMode 的运行时依赖不放的话程序启动会报找不到 DLL。注意替换 IDE 目录下的 DLL 会影响该 IDE 编译的所有项目如果只想影响单个项目把替换版 DLL 放到项目输出目录让它优先加载。4.3 用 Usage Tracker 定位未释放对象资源包里的Usage TrackerDemo 演示了一个很实用的技巧跟踪特定类的实例数量。原理是在类的NewInstance和FreeInstance里打点配合 FastMM4 的分配记录能精确知道哪个类的对象创建了没释放。Demo 里的写法可以直接抄type TTrackedObject class(TObject) public class function NewInstance: TObject; override; procedure FreeInstance; override; end; class function TTrackedObject.NewInstance: TObject; begin Result : inherited NewInstance; InterlockedIncrement(FInstanceCount); // 计数加一 end; procedure TTrackedObject.FreeInstance; begin InterlockedDecrement(FInstanceCount); // 计数减一 inherited FreeInstance; end;NewInstance是对象分配内存的入口FreeInstance是释放的入口在这两处加减计数就能知道有没有对象只增不减。InterlockedIncrement保证多线程下计数准确。跑一段时间后看FInstanceCount如果只涨不跌说明有对象泄漏再结合 FastMM4 的分配报告定位具体位置。4.4 动态加载 DLL 场景的注意事项资源包里的Dynamically Loaded DLLDemo 专门处理动态加载场景。FastMM4 默认只管主程序的内存如果 DLL 和主程序各自带了一份内存管理器跨模块传递的内存块会出问题——主程序分配的指针在 DLL 里释放两边管理器对不上直接崩。解决办法是让 DLL 和主程序共享同一个内存管理器DLL 里也引用FastMM4并且通过ShareMem或 FastMM4 自带的共享机制把管理器实例传过去。Demo 里演示了具体做法核心是保证GetMem/FreeMem的实现在两边是同一份代码。5. 避坑FastMM4 接入后最常见的五个翻车现场5.1 接入后程序启动就崩现象加完FastMM4编译通过一运行就报访问违规堆栈指向内存分配。原因FastMM4没有放在 uses 首位或者项目里同时存在其他内存管理器比如ShareMem两个管理器抢着接管初始化顺序错乱。解决确认 dpr 里FastMM4是 uses 列表第一个单元如果用了ShareMem把它去掉FastMM4 自带跨模块共享能力检查有没有第三方库偷偷引了别的内存管理器。5.2 泄漏报告里全是系统单元现象退出时报告一堆泄漏但分配位置都在System.pas、Classes.pas这些系统单元里。原因这些通常是 Delphi 运行时自己缓存的字符串、异常对象等属于“预期内不释放”不是真泄漏。FastMM4 默认会把它们也报出来。解决在FastMM4Options.inc里定义NeverUninstall或调整EnableMemoryLeakReporting的过滤规则把系统单元的分配排除掉。也可以看报告里的分配序号靠后的、业务代码里的才是重点。5.3 FullDebugMode 下程序慢到无法操作现象开了FullDebugMode后界面卡顿一个简单操作要等好几秒。原因FullDebugMode每次分配都要抓调用栈、写日志开销是正常模式的几十倍。在大量创建临时对象的循环里尤其明显。解决只在需要排查的模块上开FullDebugMode或者用FastMM_FullDebugMode.dll形态把开销转移到独立进程排查完立刻关掉。发布版本绝对不能带FullDebugMode。5.4 日志文件不生成或为空现象配置里开了LogErrorsToFile但程序目录下找不到FastMM4_Log.txt或者文件是空的。原因程序运行目录没有写权限常见于装在Program Files下或者LogFileLocation指向了一个不存在的路径。解决把LogFileLocation改成GetEnvironmentVariable(TEMP)下的路径或者程序自己的数据目录。确认路径存在且可写。这个坑很隐蔽因为 FastMM4 写日志失败时不会报错只会静默跳过。5.5 C Builder 项目接入后链接报错现象BCB 项目加了FastMM4BCB.cpp后链接阶段报重复符号或找不到GetMem。原因BCB 的内存管理入口和 Delphi 不同需要FastMM4BCB.cpp做桥接但桥接文件的编译选项或引用顺序不对。解决确认FastMM4BCB.cpp加入了工程并且它的编译顺序在其它单元之前检查 BCB 版本和资源包里对应的支持文件是否匹配BCB5、BCB6、CB2006、CB2007 各有差异必要时用 Replacement DLL 方式替代源码接入。6. 把 FastMM4 用成日常习惯一个老项目的排查流程接手那个“跑一晚上就卡死”的项目时我没有一上来就开FullDebugMode全量跑——那样程序慢到没法复现问题。我的做法是分两步先用发布期配置关FullDebugMode、开EnableMemoryLeakReporting让程序正常跑一夜第二天看FastMM4_Log.txt里的泄漏摘要确认泄漏块的大小分布和大致来源然后针对可疑模块单独开FullDebugMode用 Demo 里的 Usage Tracker 思路给关键类加实例计数缩小范围。具体操作上我会在项目里建一个独立的调试单元只在需要时引用unit MemDebug; interface procedure StartTrack; procedure StopTrackAndReport; implementation uses FastMM4, Windows, SysUtils; var FTracking: Boolean; procedure StartTrack; begin FTracking : True; // 清空日志从干净状态开始记录 if FileExists(FastMM4_Log.txt) then DeleteFile(FastMM4_Log.txt); end; procedure StopTrackAndReport; begin FTracking : False; // 触发 FastMM4 生成当前泄漏快照 if FileExists(FastMM4_Log.txt) then ShellExecute(0, open, notepad.exe, FastMM4_Log.txt, nil, SW_SHOW); end; end.StartTrack在复现操作前调用清掉旧日志StopTrackAndReport在操作后调用直接打开日志看结果。这样每次只关注一段操作产生的泄漏不会被历史数据淹没。ShellExecute那行是图省事直接弹记事本实际用的时候可以换成自己的日志分析脚本。还有一个习惯每次改完内存相关的代码编译前先确认FastMM4Options.inc的配置和当前阶段匹配。开发期忘了开FullDebugMode等于白跑发布期忘了关用户会来找你。我现在的做法是在项目里放两份FastMM4Options.inc一份FastMM4Options.debug.inc一份FastMM4Options.release.inc编译前用批处理按配置复制过去避免手改漏掉。最后说一个验证技巧FastMM4 的报告里每个泄漏块都有一个“分配序号”这个序号是全局递增的。如果你怀疑某个操作泄漏记下操作前后的序号范围在日志里搜这个范围就能精确定位到那几次分配。这个技巧在 Demo 的Usage Tracker里有体现但很多人没注意到序号的价值。从那以后我每次排查内存问题都强制先跑一遍最小复现、拿到序号范围再去看代码。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进