当前位置:首页 > 文章列表 > 文章 > 软件教程 > Visual Studio如何排查C#内存问题

Visual Studio如何排查C#内存问题

来源:17golang原创 2026-08-29 22:44:23 0浏览 收藏

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 Diagnostic Tools 中 Memory Usage 与 Take Snapshot 的真实界面

上图是微软官方 Visual Studio 界面截图。验收点不是进程内存曲线有没有波动,而是诊断窗口中已经出现 Memory Usage 和 Take Snapshot。如果没有看到这些项,先确认项目已进入调试状态,并检查诊断工具是否被关闭。

步骤二:按同一场景拍三张内存快照

程序停在第一个断点时,点击 Take Snapshot 拍基线。接着按 F5 执行一次问题场景,在结束断点再次拍快照;然后回到初始状态,用完全相同的输入再执行一次,并拍第三张快照。

三张快照的意义不同:

  • Snapshot 1:场景开始前的基线。
  • Snapshot 2:完成一次业务操作后的变化。
  • Snapshot 3:重复同一操作后的变化,用来判断对象是否逐轮累积。

Visual Studio Memory Usage 中三张快照的 Objects Diff 与 Heap Size Diff

成功状态是列表中出现多行快照,并显示 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 在每次“进入—退出”后都增加,而窗口已经关闭,这才值得继续追引用链。

Visual Studio Managed Memory 报告中的类型列表与 Paths to Root 引用链

选中可疑类型后,查看下方的 Paths to Root。它展示从该对象到垃圾回收根的持有链。只要链上仍有静态字段、长生命周期服务、事件发布者、计时器或任务回调引用对象,垃圾回收器就不能释放它。上图中上半区是类型数量和大小,下半区就是用于追根引用的界面。

步骤四:结合 Paths to Root 和 Insights 判断原因

常见 C# 内存问题通常不是“垃圾回收器失效”,而是代码仍然持有引用。看到引用链后,可以按持有者类型缩小范围:

  • 事件发布者长期存活:订阅对象离开页面时没有执行 -=,发布者继续持有订阅者。
  • 静态集合不断加入:static List、字典或缓存只有添加,没有淘汰和容量边界。
  • Timer 或回调未释放:计时器、注册回调、后台任务闭包捕获了窗口或 ViewModel。
  • 大对象被间接持有:一个看似很小的管理对象通过字段引用了大数组、图片字节或完整结果集。

在 Managed Memory 报告中切换到 Insights,Visual Studio 还会按当前快照给出可用的自动分析项,例如 Duplicate Strings、Sparse Arrays,以及部分版本和场景下的事件处理器泄漏提示。它适合帮助缩小方向,但不能替代引用链和代码检查。

Visual Studio Managed Memory Insights 中 Duplicate Strings 与 Sparse Arrays 结果

上图的可见验收点是 Insights 页已经列出具体类型、浪费字节和数量。比如重复字符串很多,优化方向可能是减少重复构造或调整缓存;稀疏数组占用明显,则要检查是否用过大的数组承载少量元素。不要看到建议就立刻大改,先回到自己的类型和调用路径验证。

修复后如何确认内存问题真的解决

修复事件退订、集合清理、缓存淘汰、IDisposable.Dispose 或计时器停止逻辑后,重新启动同一调试会话,并重复完全相同的输入与操作次数。验收时重点看三个结果:

  1. 第二轮、第三轮的 Objects (Diff) 不再按固定数量累积。
  2. 可疑业务类型的 Paths to Root 不再指向旧页面、旧 ViewModel 或已经结束的任务。
  3. 一次性缓存增长后趋于稳定,而不是每执行一次就增加一份。

如果对象数量已经稳定,但进程工作集没有立刻下降,也不要据此判断修复失败。.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 找到真正持有它的对象。修复后按原路径复测,差值不再逐轮累积,才算完成一次可靠的内存问题闭环。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python asyncio.Semaphore 控制多模型并发:避免推理请求同时打满显存Python asyncio.Semaphore 控制多模型并发:避免推理请求同时打满显存
上一篇
Python asyncio.Semaphore 控制多模型并发:避免推理请求同时打满显存
Java CharsetDecoder 怎么定位乱码:REPORT 模式、ByteBuffer 边界与解码结果
下一篇
Java CharsetDecoder 怎么定位乱码:REPORT 模式、ByteBuffer 边界与解码结果
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    468次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    530次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    549次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    499次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    317次使用