24. 内存泄漏检测:Visual Studio诊断工具
内存泄漏这玩意儿,说大不大,说小不小。我见过线上服务跑了一周突然OOM,查了半天发现是某个定时器里new出来的对象没人管。也见过客户端软件越用越卡,最后用户骂娘——说白了,就是内存只借不还。
Visual Studio自带的诊断工具,其实挺强的。很多人只知道用它看CPU占用,却忽略了它抓内存泄漏的能力。今天我就带你把它用透。
24.1 诊断工具的基本用法
先说说怎么打开。调试的时候,点菜单栏的「调试」→「性能探测器」,或者直接按 Alt+F2。你会看到一个窗口,里面有几个选项:
- CPU使用率:看哪个函数吃CPU
- 内存使用率:看堆上分配了多少
- 应用程序时间线:看UI响应情况
我们重点关注「内存使用率」。勾上它,点「开始」,程序就跑起来了。跑一会儿,点「获取快照」,就能看到当前堆上的内存分配情况。
24.2 快照对比:找出泄漏的根
快照拍好了,怎么用?看下面这张图:
操作很简单:在「内存使用率」面板里,右键点击快照2,选「与快照1比较」。VS会自动算出差值,列出新增的对象类型和数量。
我遇到过最典型的情况:每次打开一个对话框,堆上就多出几个 std::string 和自定义对象。关闭对话框后,这些对象没被释放。一查代码,原来是对话框的析构函数里忘了 delete 某个成员指针。
24.3 查看堆分配详情
光知道多了哪些类型还不够,你得找到具体是哪行代码分配的内存。VS的诊断工具支持「查看分配调用堆栈」。
在快照对比结果里,双击某个类型(比如 MyDataObject),会弹出一个窗口,列出所有该类型的实例。选中一个实例,下面会显示它的分配调用堆栈:
// 假设你看到这样的调用堆栈
MyDataObject::MyDataObject() line 42
MainWindow::OnButtonClick() line 156
MainWindow::OnCommand() line 89
...
看到 MainWindow::OnButtonClick() line 156,你直接双击就能跳到源码。嗯,这里要注意:VS的调用堆栈有时候会缺帧,尤其是用了Release模式或者优化选项。我建议调试时用Debug模式,符号加载完整,堆栈才靠谱。
24.4 使用CRT调试堆
除了诊断工具,Visual Studio还自带一套C运行时库的调试堆函数。说白了,就是 malloc 和 new 的调试版本,能帮你追踪内存泄漏。
用法很简单,在代码开头加上:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
// 在程序入口处调用
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
程序退出时,如果输出窗口里出现类似这样的信息:
Detected memory leaks!
Dumping objects ->
{145} normal block at 0x0078C8A0, 40 bytes long.
Data: < ... >
Object dump complete.
那恭喜你,抓到泄漏了。那个 {145} 是分配序号,你可以用 _CrtSetBreakAlloc(145) 让程序在分配第145块内存时断下来,直接看调用堆栈。
_CrtSetBreakAlloc 配合条件断点,半小时就定位到了泄漏点。比肉眼翻代码快多了。
24.5 实战:一个泄漏示例
光说不练假把式。我们写个简单的泄漏程序,然后用VS诊断工具抓它:
#include <vector>
#include <string>
class LeakyObject {
public:
LeakyObject() {
data = new char[1024]; // 分配了,但析构函数没释放
}
// 注意:没有定义析构函数!
private:
char* data;
};
void SimulateLeak() {
std::vector<LeakyObject*> objects;
for (int i = 0; i < 100; ++i) {
objects.push_back(new LeakyObject());
}
// 忘记 delete 每个对象
// 也忘记 clear() 前先释放
}
int main() {
SimulateLeak();
// 程序退出时,100个LeakyObject泄漏
return 0;
}
用VS诊断工具跑这个程序:
- 在
SimulateLeak()调用前拍快照1 - 调用后拍快照2
- 对比快照,看到新增了100个
LeakyObject实例 - 双击查看调用堆栈,定位到
new LeakyObject()那行
你看,整个过程不到两分钟。如果代码里写了析构函数 delete[] data,并且在 main 结束前遍历vector释放每个对象,泄漏就解决了。
24.6 常见陷阱与避坑指南
用VS诊断工具时,有几个坑我踩过,分享给你:
- Release模式下的堆栈不准:编译器会内联函数、优化变量,导致调用堆栈残缺不全。调试时务必用Debug模式。
- 第三方库的泄漏:有时候泄漏不在你的代码里,而在某个DLL里。VS诊断工具可以显示模块名,帮你区分是自家代码还是外部库的问题。
- STL容器的误报:某些STL实现会在内部缓存已释放的内存,导致诊断工具报告「疑似泄漏」。这时候可以调低
_CRTDBG_ALLOC_MEM_DF的检测级别,或者用_CrtSetReportMode过滤掉已知的误报。
std::string 的内部缓冲区。原来是我在循环里频繁创建临时string,STL的small string optimization(SSO)导致一些缓冲区没被立即回收。这不是真正的泄漏,只是诊断工具的「过度敏感」。后来我改用 _CrtMemCheckpoint 做精确对比,才过滤掉这些噪音。
24.7 总结一下
Visual Studio的诊断工具,说白了就是三招:
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 内存使用率快照 | 快速定位泄漏类型 | 操作简单,可视化强 | 需要手动对比 |
| 分配调用堆栈 | 定位泄漏代码行 | 精确到源码行 | 需要启用堆跟踪 |
| CRT调试堆 | 自动化检测泄漏 | 程序退出时自动报告 | 有误报可能 |
我个人习惯是:先用快照对比看有没有泄漏,再用调用堆栈定位代码,最后用CRT调试堆做回归验证。三管齐下,基本没有抓不到的泄漏。
你想想看,如果每次写完代码都能顺手跑一遍诊断工具,很多线上问题根本不会发生。嗯,这就是专业和业余的区别。
公众号:蓝海资料掘金营,微信deep3321