在 Visual Studio 中按 Ctrl+Alt+F2 打开诊断工具,点击“内存使用率”下的“拍摄快照”按钮(需 Debug 配置、.NET Core 3.1+/5+),待疑似泄漏逻辑执行后再拍第二张对比;重点关注 Count Diff、Size Diff 和 Inclusive Size,右键类型查看引用链,警惕 Finalizer Queue 堆积和弱引用误判。

怎么在 Visual Studio 里快速启动内存分析器
Visual Studio 自带的诊断工具(Diagnostic Tools)和 .NET Memory Profiler 能直接捕获托管堆快照,不需要额外安装插件(VS 2019 及以后版本默认启用)。关键前提是项目必须以 Debug 配置运行,且目标框架为 .NET Core 3.1+ 或 .NET 5+(.NET Framework 仅支持部分功能,且需手动启用 GC 回收日志)。
操作路径:调试时按 Ctrl+Alt+F2 打开诊断工具窗口 → 点击“内存使用率”图表下方的“拍摄快照”按钮。注意:不要在程序刚启动时立刻拍,等疑似泄漏的逻辑执行完、对象理应被释放后,再拍第二张快照做对比。
- 若看不到“内存使用率”选项,检查是否启用了“启用诊断工具”(
工具 → 选项 → 调试 → 常规 → 启用诊断工具) - ASP.NET Core 项目需确保未启用
dotnet watch,否则快照可能失败并报错Unable to collect memory data: process is not in a debuggable state - 控制台或 Windows Forms 应用需保持进程活跃(比如加个
Console.ReadLine()),否则调试器断开后无法采集
怎么看快照对比找出泄漏对象
两张快照之间,真正可疑的是“新分配但未释放”的对象——不是数量多的类,而是“增长量大 + 实例长期存活 + 类型明显不该常驻”的对象。比如 HttpClient 实例在 Web API 客户端里持续增长,或自定义的 EventHandler 持有窗体引用却没反注册。
在快照对比视图中,重点关注三列:Count Diff(实例数变化)、Size Diff(字节变化)、Inclusive Size(含子对象总大小)。右键某类型 → “查看对实例的引用”,可看到谁持有它(比如 static Dictionary 或未注销的 += 事件)。
- 警惕
Finalizer Queue里堆积的对象:说明 GC 已标记回收,但终结器线程卡住或未运行,常见于重写了Finalize()却没调用GC.SuppressFinalize() -
WeakReference对象本身不阻止回收,但它的Target字段若非 null,就代表背后对象还活着——容易误判为“弱引用失效”,实则是强引用残留 - 字符串(
String)大量增长时,先查StringBuilder.ToString()是否被缓存,或日志组件是否把消息拼接后长期存进集合
为什么用 dotMemory 或 PerfView 补充分析
Visual Studio 内存分析器对大堆(>2GB)或高频率分配场景响应慢,且不支持导出完整对象图或跨进程追踪。这时候需要更底层的工具。
PerfView 是微软免费命令行工具,适合抓取 GC 行为:运行 PerfView /nogui /accepteula collect,复现操作后按 Ctrl+Shift+1 停止,打开 GCStats 视图看 % Time in GC 是否异常高(>10% 值得怀疑);再双击 HeapAllocStacks 查看哪些调用栈分配最多内存。
dotMemory(JetBrains)优势在于能标记“根路径”(Root Path)并高亮循环引用链,比如 A → B → C → A 这种 VS 默认不提示的隐式强引用。
- 用
dotMemory时务必勾选“Analyze memory traffic”,否则只看到静态快照,漏掉短生命周期对象的累积效应 -
PerfView分析需开启GC Heap Collect事件(默认关闭),否则堆对象明细为空 - 所有工具都依赖
Debug构建,Release下 JIT 优化可能导致内联或变量提前释放,让泄漏“消失”——这不是修复,是掩盖
常见误判和绕不开的坑
很多“疑似泄漏”其实是预期行为:比如 ThreadPool 线程长时间空闲会保活、String.Intern() 缓存永久驻留、AssemblyLoadContext 在 .NET Core 中默认不卸载。判断前先确认是否真违反设计契约。
-
Task对象本身不泄漏,但未 await 的Task若内部持有了CancellationTokenSource或闭包变量,会导致后者无法回收 - WPF 的
Binding和Command默认强引用控件,用RelativeSource或静态资源时尤其要注意生命周期匹配 - 第三方库(如 Entity Framework 的
DbContext)若被注入为 Singleton,其内部跟踪器会不断积累实体引用——这不是你的代码写错了,是 DI 生命周期配置错了
最麻烦的情况是:快照里找不到明显增长类型,但私有字节(Private Bytes)持续上涨。这时大概率是本机资源泄漏(SafeHandle 未释放、Marshal.AllocHGlobal 忘了 FreeHGlobal),得切到“本机堆”视图或用 Process Explorer 查句柄数。









