勇哥注:
此文转载自:https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/
任何从事过大型企业项目的人都知道内存泄漏就像大酒店里的老鼠。当它们很少时,您可能不会注意到,但是您必须始终保持警惕,以防它们人口过多,闯入厨房并在所有东西上大便。
查找、修复和学习避免内存泄漏是一项重要技能。我将列出我和高级 .NET 开发人员使用的 8 种最佳实践技术,这些技术为我提供了本文的建议。这些技术将教您检测应用程序中何时出现内存泄漏问题,找到特定的内存泄漏并修复它。最后,我将包括监控和报告已部署程序的内存泄漏的策略。
在 .NET 中定义内存泄漏
在垃圾收集环境中,内存泄漏这个词有点
这有两个相关的核心原因。第一个核心原因是您的对象仍被引用但实际上未使用。由于它们被引用,垃圾收集器不会收集它们,它们将永远保留,占用内存。例如,当您注册事件但从未取消注册时,可能会发生这种情况。
第二个原因是当您以某种方式分配非托管内存(没有垃圾收集)并且不释放它时。这并不难做到。.NET 本身有很多分配非托管内存的类。几乎所有涉及流、图形、文件系统或网络调用的事物都在幕后进行。通常,这些类实现一个 Dispose 方法,该方法释放内存(我们稍后会讨论)。您可以使用特殊的 .NET 类(如 Marshal)或 PInvoke(后面有一个示例)轻松地自己分配非托管内存。
让我们继续我的最佳实践技术列表:
1. 使用诊断工具窗口检测内存泄漏问题
如果你去调试| 窗户| 显示诊断工具,你会看到这个窗口。如果你和我一样,你可能在安装 Visual Studio 后看到了这个工具窗口,立即关闭了它,再也没有想过它。不过,诊断工具窗口可能非常有用。它可以轻松帮助您检测 2 个问题:内存泄漏和GC 压力。
当您有内存泄漏时,进程内存图如下所示:

您可以看到顶部的黄线表示 GC 正在尝试释放内存,但它仍在不断上升。
当您有GC Pressure 时,进程内存图如下所示:

GC 压力是指您创建新对象并过快地处理它们以致垃圾收集器跟不上。如图所示,内存接近极限,GC 爆发非常频繁。
您将无法通过这种方式找到特定的内存泄漏,但是您可以检测到您有内存泄漏问题,这本身就很有用。在Enterprise Visual Studio 中,诊断窗口还包括一个内置的内存分析器,它允许查找特定的泄漏。我们将在最佳实践 #3 中讨论内存分析。
2. 使用任务管理器、进程资源管理器或 PerfMon 检测内存泄漏问题
检测主要内存泄漏问题的第二种最简单的方法是使用任务管理器或进程资源管理器(来自 SysInternals)。这些工具可以显示您的进程使用的内存量。如果它随着时间的推移持续增加,则可能存在内存泄漏。

PerfMon有点难以使用,但可以显示随时间推移的内存使用情况的漂亮图表。这是我的应用程序的图表,它无休止地分配内存而不释放它。我正在使用流程| 专用字节计数器。

请注意,这种方法是出了名的不可靠。您可能会因为 GC 尚未收集内存而增加内存使用量。还有共享内存和私有内存的问题,因此您可能会错过内存泄漏和/或诊断不属于您自己的内存泄漏(解释)。最后,您可能会将内存泄漏误认为GC Pressure。在这种情况下,您没有内存泄漏,但是您创建和处理对象的速度太快以至于 GC 跟不上。
尽管有缺点,我还是提到了这种技术,因为它既易于使用,有时也是您唯一的工具。在长时间观察时,这也是一个不错的指标。
3. 使用内存分析器检测内存泄漏
内存分析器就像处理内存泄漏的厨师刀。它是查找和修复它们的主要工具。虽然其他技术更容易使用或更便宜(分析器许可证很昂贵),但最好通过至少一个内存分析器来有效解决内存泄漏问题而精通。
.NET 内存分析器中的大牌
所有内存分析器都以类似的方式工作。您可以附加到正在运行的进程或打开转储文件。探查器将创建进程当前内存堆的快照。您可以通过各种方式分析快照 例如,这里是当前快照中所有已分配对象的列表:

您可以看到每种类型分配了多少个实例,它们占用了多少内存,以及GC Root的引用路径。
GC Root 是 GC 无法释放的对象,因此也无法释放 GC 根引用的任何对象。当前活动线程中的静态对象和本地对象是 GC Roots。在了解 .NET 中的垃圾收集中了解更多信息。
最快和最有用的分析技术是比较内存应该返回到相同状态的 2 个快照。第一个快照是在操作之前拍摄的,另一个快照是在操作之后拍摄的。具体步骤是:
从应用程序中的某种空闲状态开始。这可能是主菜单或类似的东西。
通过附加到进程或保存转储,使用内存分析器拍摄快照。
运行您怀疑造成内存泄漏的操作。在它结束时返回到空闲状态。
拍摄第二张快照。
将两个快照与您的内存分析器进行比较。
调查新创建的实例,它们可能是内存泄漏。检查“GC Root 的路径”并尝试理解为什么这些对象没有被释放。
这是一个很棒的视频,其中在SciTech 内存分析器 中比较了 2 个快照并发现了内存泄漏:
4. 使用“Make Object ID”查找内存泄漏
在我上一篇文章5 在 C# .NET 中通过事件避免内存泄漏的技术中,您应该知道 我展示了一种通过在类 Finalizer 中放置断点来查找内存泄漏的技术。我将在这里向您展示一个类似的方法,它更易于使用且不需要更改代码。这个利用了调试器的Make Object ID功能和Immediate Window。
假设您怀疑某个类存在内存泄漏。换句话说,您怀疑在运行某个场景后,这个类会一直被引用而不会被 GC 收集。要了解 GC 是否确实收集了它,请执行以下步骤:
在创建类的实例的地方放置一个断点。
将鼠标悬停在变量上以打开调试器的数据提示,然后右键单击并使用Make Object ID。您可以在立即窗口中键入$1以查看对象 ID 是否已正确创建。
完成应该从引用中释放您的实例的场景。
使用已知的魔法线强制 GC 收集
5.在立即窗口中再次输入$1。如果它返回 null,则 GC 收集了您的对象。如果没有,则内存泄漏。
这是我调试存在内存泄漏的场景:
这是我调试一个没有内存泄漏的类似场景:
您可以通过在立即窗口中键入魔术行来最终强制进行垃圾收集,从而使该技术成为一种完全调试体验,无需更改代码。
重要提示:此做法在 .NET Core 2.X 调试器 ( issue ) 中效果不佳。在与对象分配相同的范围内强制垃圾收集不会释放该对象。您可以通过在超出范围的另一种方法中强制进行垃圾收集来花费更多的精力。
5. 注意常见的内存泄漏源
总是有导致内存泄漏的风险,但某些模式更有可能这样做。我建议在使用这些时要格外小心,并使用最后一个最佳实践等技术主动检查内存泄漏。
以下是一些较常见的罪犯:
.NET中的事件因导致内存泄漏而臭名昭著。你可以无辜地订阅一个事件,甚至不用怀疑就造成了破坏性的内存泄漏。这个主题非常重要,我专门写了一整篇文章来讨论它:您应该知道的 5 种技术,以避免 C# .NET 中的事件导致内存泄漏
特别是静态变量、集合和静态事件应该总是看起来很可疑。请记住,所有静态变量都是GC Roots,因此它们永远不会被 GC 收集。
缓存功能——任何类型的缓存机制都容易导致内存泄漏。通过将缓存信息存储在内存中,最终它会填满并导致OutOfMemory异常。解决方案可以是定期删除旧缓存或限制缓存量。
WPF 绑定可能很危险。经验法则是始终绑定到DependencyObject或绑定到
一种 INotifyPropertyChanged 目的。如果不这样做,WPF 将从静态变量创建对绑定源(即 ViewModel)的强引用,从而导致内存泄漏。此有用的 StackOverflow 线程中有关 WPF 绑定泄漏的更多信息
捕获的成员- 事件处理程序方法可能很明显意味着引用了一个对象,但是当一个变量在匿名方法中被捕获时,它也会被引用。下面是一个内存泄漏的例子:
永不终止的线程-每个线程的Live Stack被视为GC Root。这意味着在线程终止之前,GC 不会收集来自其堆栈上变量的任何引用。这也包括定时器。如果您的 Timer 的 Tick Handler 是一个方法,则该方法的对象被视为已引用且不会被收集。下面是一个内存泄漏的例子:
有关此主题的更多信息,请查看我的文章.NET 中可能导致内存泄漏的 8 种方式。
6. 使用 Dispose 模式防止非托管内存泄漏
您的 .NET 应用程序不断使用非托管资源。.NET 框架本身严重依赖非托管代码进行内部操作、优化和 Win32 API。任何时候使用Streams、Graphics或Files
使用非托管代码的 .NET 框架类通常实现IDisposable。那是因为非托管资源需要
在使用语句转换的代码放到一个尝试/最后的场景,在后面声明的Dispose方法被调用的最后 条款。
但是,即使您不调用Dispose方法,这些资源也会被释放,因为 .NET 类使用Dispose Pattern。这基本上意味着如果之前没有调用 Dispose,则在对象被垃圾回收时从Finalizer调用它。也就是说,如果您没有内存泄漏并且确实调用了 Finalizer。
当您自己分配非托管资源时,您绝对应该使用 Dispose 模式。下面是一个例子:
这种模式的重点是允许显式处置资源。但是还要添加一个保护措施,如果没有调用Dispose(),您的资源将在垃圾收集期间(在终结器中)被处理。
该GC.SuppressFinalize(这)也很重要。如果对象已经存在,它可以确保垃圾回收时不会调用 Finalizer
7.从代码添加内存遥测
有时,您可能希望定期记录内存使用情况。也许您怀疑您的生产服务器存在内存泄漏。也许你想在你的记忆力达到一定限度时采取一些行动。或者也许您只是养成了监控记忆的好习惯。
我们可以从应用程序本身获得很多信息。获取当前正在使用的内存非常简单:
有关更多信息,您可以使用用于PerfMon的PerformanceCounter类:
来自任何 perfMon 计数器的信息都可用,这是很多的。
你可以更深入。CLR MD (Microsoft.Diagnostics.Runtime) 允许您检查当前的内存堆并获取任何可能的信息。例如,您可以打印内存中所有已分配的类型,包括实例计数、根路径等。你几乎从代码中得到了一个内存分析器。
要了解使用 CLR MD 可以实现的目标,请查看 Dudi Keleti 的DumpMiner。
所有这些信息都可以记录到一个文件中,或者甚至更好地记录到 Application Insights 等遥测工具中。
8. 测试内存泄漏
主动测试内存泄漏是一个很好的做法。这并不难。这是您可以使用的简短模式:
为了进行更深入的测试,像 SciTech 的.NET Memory Profiler和dotMemory这样的内存分析器提供了一个测试 API:
概括
不了解你,但我的新年决心是:更好的内存管理。
我希望这篇文章能给你一些价值,如果你订阅我的博客或在下面发表评论,我会很高兴的。欢迎任何反馈。

