Visual Studio如何排查C#内存问题
来源:17golang原创
时间:2026-08-29 22:44:23 225浏览 收藏
C# 程序的内存曲线偶尔升高并不等于泄漏,真正值得追的是:同一业务动作执行多次后,对象数量和托管堆持续增长,而且退出页面、清空集合或等待垃圾回收后仍不回落。Visual Studio 的 Memory Usage 可以把这件事拆成可验证的步骤:先拍基线,再重放场景,比较快照,最后沿 Paths to Root 找到仍在持有对象的引用。
实践要点
- 先固定一条可重复的业务路径,不要一上来就盯整个进程的总内存。
- 至少拍两张快照;更稳妥的做法是基线、第一次操作、第二次操作各拍一张。
- 优先看
Objects (Diff)与Heap Size (Diff)连续增长的类型。 Paths to Root才能回答对象为什么还活着,单看对象大小不能直接判定泄漏。- 修改代码后必须用同一条操作路径复测,确认差值不再逐轮累积。
步骤一:在 Visual Studio 打开 Memory Usage
先打开要排查的 C# 项目,把问题场景缩小到一条能重复执行的路径,例如“进入订单详情—返回列表”“打开窗口—关闭窗口”或“刷新数据—清空结果”。在场景开始前和结束后各设置一个断点,避免初始化、登录和后台任务的分配噪声混进结果。
启动调试后,按菜单路径 Debug > Windows > Show Diagnostic Tools 打开诊断窗口;如果窗口已经显示,直接切到 Memory Usage。在 Summary 区域能看到 Take Snapshot,说明内存快照工具已经进入可用状态。

上图是微软官方 Visual Studio 界面截图。验收点不是进程内存曲线有没有波动,而是诊断窗口中已经出现 Memory Usage 和 Take Snapshot。如果没有看到这些项,先确认项目已进入调试状态,并检查诊断工具是否被关闭。
步骤二:按同一场景拍三张内存快照
程序停在第一个断点时,点击 Take Snapshot 拍基线。接着按 F5 执行一次问题场景,在结束断点再次拍快照;然后回到初始状态,用完全相同的输入再执行一次,并拍第三张快照。
三张快照的意义不同:
- Snapshot 1:场景开始前的基线。
- Snapshot 2:完成一次业务操作后的变化。
- Snapshot 3:重复同一操作后的变化,用来判断对象是否逐轮累积。

成功状态是列表中出现多行快照,并显示 Objects (Diff) 和 Heap Size (Diff)。这里先别急着把红色上箭头当成泄漏。页面首次打开、JIT 编译、缓存预热和一次性元数据加载都可能让第二张快照增长;更有价值的是第二、第三次重复后,同一批类型仍按相近幅度上涨。
步骤三:从 Objects Diff 找到持续增长的类型
点击某张快照的 Objects (Diff) 数字,可以按对象数量差异打开报告;点击 Heap Size (Diff) 则更适合找单个对象不多、但占用字节明显的类型。排查 C# 托管内存时,先观察下面四列:
| 界面字段 | 它回答的问题 | 判断方式 |
|---|---|---|
| Count | 这种对象还活着多少个 | 重复场景后是否持续增加 |
| Size (Bytes) | 对象自身占用多少托管内存 | 适合识别大量小对象 |
| Inclusive Size | 对象连同其引用对象共占多少内存 | 适合发现持有大对象图的入口 |
| Compare With Baseline | 当前报告与哪张快照比较 | 确保比较的是相邻业务阶段 |
可以在 Filter types 中输入自己项目的命名空间,例如 OrderClient.,先排除框架初始化对象。若一个 OrderViewModel 在每次“进入—退出”后都增加,而窗口已经关闭,这才值得继续追引用链。

选中可疑类型后,查看下方的 Paths to Root。它展示从该对象到垃圾回收根的持有链。只要链上仍有静态字段、长生命周期服务、事件发布者、计时器或任务回调引用对象,垃圾回收器就不能释放它。上图中上半区是类型数量和大小,下半区就是用于追根引用的界面。
步骤四:结合 Paths to Root 和 Insights 判断原因
常见 C# 内存问题通常不是“垃圾回收器失效”,而是代码仍然持有引用。看到引用链后,可以按持有者类型缩小范围:
- 事件发布者长期存活:订阅对象离开页面时没有执行
-=,发布者继续持有订阅者。 - 静态集合不断加入:
static List、字典或缓存只有添加,没有淘汰和容量边界。 - Timer 或回调未释放:计时器、注册回调、后台任务闭包捕获了窗口或 ViewModel。
- 大对象被间接持有:一个看似很小的管理对象通过字段引用了大数组、图片字节或完整结果集。
在 Managed Memory 报告中切换到 Insights,Visual Studio 还会按当前快照给出可用的自动分析项,例如 Duplicate Strings、Sparse Arrays,以及部分版本和场景下的事件处理器泄漏提示。它适合帮助缩小方向,但不能替代引用链和代码检查。

上图的可见验收点是 Insights 页已经列出具体类型、浪费字节和数量。比如重复字符串很多,优化方向可能是减少重复构造或调整缓存;稀疏数组占用明显,则要检查是否用过大的数组承载少量元素。不要看到建议就立刻大改,先回到自己的类型和调用路径验证。
修复后如何确认内存问题真的解决
修复事件退订、集合清理、缓存淘汰、IDisposable.Dispose 或计时器停止逻辑后,重新启动同一调试会话,并重复完全相同的输入与操作次数。验收时重点看三个结果:
- 第二轮、第三轮的 Objects (Diff) 不再按固定数量累积。
- 可疑业务类型的 Paths to Root 不再指向旧页面、旧 ViewModel 或已经结束的任务。
- 一次性缓存增长后趋于稳定,而不是每执行一次就增加一份。
如果对象数量已经稳定,但进程工作集没有立刻下降,也不要据此判断修复失败。.NET 运行时可能保留已提交内存供后续分配使用。此时更应该看托管堆快照中的存活对象和差异,而不是只看任务管理器里的进程内存。
常见问题
为什么每次快照都会多出一些对象?
调试器、诊断工具、JIT、日志和缓存都可能产生额外对象。把场景做小、设置前后断点并重复两到三次,可以把一次性噪声与持续累积分开。
Objects Diff 增长但 Heap Size Diff 很小,需要处理吗?
要结合对象类型判断。大量短小对象可能只增加少量字节,但如果它们代表已关闭窗口、请求上下文或事件订阅者,仍然说明生命周期有问题。
Heap Size Diff 很大,但对象数量变化不多是什么原因?
通常要检查大数组、字符串、图片字节、缓存结果或某个对象持有的大对象图。此时 Inclusive Size 比单纯的 Count 更有参考价值。
应该用调试器里的 Memory Usage,还是 Performance Profiler?
需要断点、单步和在特定代码位置拍快照时,用调试器集成的 Memory Usage 更直接;要分析 Release 构建、减少调试器影响或做更完整的性能采集时,可以从 Debug > Performance Profiler 启动 Memory Usage。
排查结果整理
Visual Studio 排查 C# 内存问题的关键不是拍一张快照后找“最大的类型”,而是控制变量:用同一场景建立基线,比较连续快照,锁定持续增长的业务类型,再沿 Paths to Root 找到真正持有它的对象。修复后按原路径复测,差值不再逐轮累积,才算完成一次可靠的内存问题闭环。
-
337 收藏
-
184 收藏
-
488 收藏
-
259 收藏
-
233 收藏
-
250 收藏
-
437 收藏
-
202 收藏
-
297 收藏
-
333 收藏
-
412 收藏
-
424 收藏
-
376 收藏
-
471 收藏
-
199 收藏
-
131 收藏
-
220 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习