如何查找,修复和避免C#.NET中内存泄漏的8个最佳实践

原文来自互联网,由长沙DotNET技术社区编译。

本文来源:https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/

从事大型企业项目的任何人都知道内存泄漏就像是大型酒店中的老鼠。当它们很少时,您可能不会注意到,但是您必须始终保持警惕,以防它们过多,闯入厨房并制造混乱。

查找,修复和学习避免内存泄漏是一项重要技能。

我将列出我和高级.NET开发人员为我提供建议的8种最佳实践技术,这些技术将教您检测应用程序中何时存在内存泄漏问题,查找特定的内存泄漏并进行修复。最后,我将介绍监视和报告已部署程序的内存泄漏的策略。

定义.NET中的内存泄漏

在垃圾收集环境中,术语“内存泄漏”有点违反直觉。当有垃圾收集器(GC)负责收集所有内容时,我的内存为何还会泄漏?

有两个相关的核心原因。第一个核心原因是当您具有仍被引用但实际上未使用的对象时。由于已引用它们,垃圾收集器将不会收集它们,并且它们将永久保留,占用内存。例如,当您注册事件但从不注销时,可能会发生这种情况。

第二个原因是当您以某种方式分配非托管内存(没有垃圾回收)并且不释放它时。这并不难做到。.NET本身有很多分配非托管内存的类。几乎所有涉及流,图形,文件系统或网络调用的操作都是在后台进行的。通常,这些类实现** Dispose **方法,该方法释放内存(稍后再讨论)。您可以使用特殊的.NET类(如Marshal)或PInvoke(有一个进一步的示例)轻松地自己分配非托管内存。

让我们进入我的最佳实践技术列表:

1.使用诊断工具窗口检测内存泄漏问题

如果您去调试 | Windows | 显示诊断工具,您将看到此窗口。如果您像我一样,则可能在安装Visual Studio之后看到了此工具窗口,立即关闭了它,再也没有想到它。诊断工具窗口可能会非常有用。它可以轻松地帮助您检测两个问题:内存泄漏和GC压力。

当您有内存泄漏时,“进程内存”图如下所示:

图片

从顶部的黄线可以看到GC正在尝试释放内存,但它仍在不断上升。

当您具有GC Pressure时,过程内存图如下所示:

图片

“ GC压力”是在创建新对象并将它们处置得太快而导致垃圾收集器无法跟上时。如图所示,内存已接近极限,GC突发非常频繁。

您将无法通过这种方式找到特定的内存泄漏,但是您可以检测到内存泄漏问题,这本身就很有用。在Enterprise Visual Studio中,“诊断”窗口还包括一个内置的内存探查器,该探查器确实可以查找特定的泄漏。我们将在最佳实践#3中讨论内存分析。

2.使用任务管理器,Process Explorer或PerfMon检测内存泄漏问题

检测主要内存泄漏问题的第二种最简单方法是使用任务管理器或Process Explorer(来自SysInternals)。这些工具可以显示您的进程使用的内存量。如果它随着时间不断增加,则可能是内存泄漏。

图片

性能监视器是有点难以利用[1],但能证明你的内存使用量随时间的一个很好的曲线图。这是我的应用程序的图形,它不停地分配内存而不释放它。我正在使用过程 | 专用字节计数器。

图片

请注意,此方法众所周知是不可靠的。您可能只是因为GC尚未收集内存而增加了内存使用量。还有共享内存和私有内存的问题,因此您可能会错过内存泄漏和/或诊断不是您自己的内存泄漏(说明[2])。最后,您可能将内存泄漏误认为是GC Pressure。在这种情况下,您不会发生内存泄漏,但是创建和处理对象的速度如此之快,以至于GC无法跟上进度。

尽管有缺点,但我还是提到了这种技术,因为它既易于使用,有时又是唯一的工具。这也是一个不错的指标,长时间观察时出了点问题。

3.使用内存分析器检测内存泄漏

内存分析器就像处理内存泄漏的厨师刀。它是查找和修复它们的主要工具。尽管其他技术可能更易于使用或更便宜(探查器许可证价格昂贵),但最好精通至少一个内存探查器以有效解决内存泄漏问题。

.NET内存分析器中的大人物 是:dotMemory,SciTech内存分析器 和 ANTS Memory Profiler。如果您拥有Visual Studio Enterprise,则还有一个“免费”分析器。

所有内存分析器都以类似的方式工作。您可以附加到正在运行的进程,也可以打开转储文件。探查器将为您的进程的当前内存堆创建一个快照。您可以通过各种方式分析快照,例如,以下是当前快照中所有已分配对象的列表:

图片

您可以看到每种类型分配了多少实例,它们占用了多少内存以及GC Root的引用路径。

GC根是GC无法释放的对象,因此GC根引用所引用的所有内容也无法释放。当前活动线程的静态对象和本地对象是GC根。在了解.NET中的垃圾收集中了解更多信息。

最快,最有用的性能分析技术是比较内存应返回相同状态的2个快照。在操作之前拍摄第一个快照,在操作之后拍摄另一个快照。确切的步骤是:

1.从应用程序中的某种空闲状态开始。这可能是主菜单或类似的东西。2.通过附加到进程或保存转储,使用Memory Profiler拍摄快照。3.运行怀疑会导致内存泄漏的操作。返回到空闲状态。4.拍摄第二张快照。5.将这两个快照与您的内存分析器进行比较。6.研究新创建的实例,它们很可能是内存泄漏。检查“ GC根目录的路径”,并尝试了解为什么未释放这些对象。

这是一个很棒的视频,其中在SciTech内存分析器 中比较了2个快照,并发现了内存泄漏:

4.使用“ Make Object ID”查找内存泄漏

在上一篇文章5避免C#.NET中的事件造成内存泄漏的技术中,您应该知道[3] 我展示了一种通过在类Finalizer中放置断点来查找内存泄漏的技术。在这里,我将向您展示一种类似的方法,该方法更易于使用,并且不需要更改代码。这利用了调试器的Make Object ID功能和Instant Window。

假设您怀疑某个类存在内存泄漏。换句话说,您怀疑在运行特定方案后,此类仍保持引用状态,并且GC从未收集过此类。要确定GC是否真正收集了它,请按照下列步骤操作:

1.在创建类实例的地方放置一个断点。2.将鼠标悬停在变量上以打开调试器的数据提示,然后右键单击并使用Make Object ID。您可以在立即窗口$ 1中键入以查看是否正确创建了对象ID。3.完成本应从实例中释放实例的方案。4.使用已知的魔术线强制进行GC收集

GC.Collect();   
GC.WaitForPendingFinalizers();   
GC.Collect();

5. 原文中有两端视频,由于微信审核的缘故,无法上传,有兴趣的可以查看原文。

参考这个流程,您可以通过在立即窗口中键入魔术线来强制进行垃圾收集,从而使该技术成为完全的调试体验,而无需更改代码。

重要提示:这种做法在.NET Core 2.X调试器(问题[4])中不能很好地工作。强制在与对象分配相同的范围内进行垃圾回收不会释放该对象。通过将另一种方法中的垃圾回收强制超出范围,您可以花费更多的精力。

5.当心常见的内存泄漏源

始终存在导致内存泄漏的风险,但是某些模式更有可能造成内存泄漏。我建议在使用这些工具时要格外小心,并使用最新的最佳做法等技术来主动检查内存泄漏。

以下是一些较常见的违规者:

•.NET中的事件因导致内存泄漏而臭名昭著。您可以无辜地订阅一个事件,甚至在不怀疑的情况下导致破坏性的内存泄漏。这个主题是如此重要,以至于我专门写了整篇文章:您应该知道的5种避免C#.NET中的事件造成内存泄漏的技术[5]•特别是静态变量,集合和静态事件应该总是看起来可疑。请记住,所有静态变量都是GC根,因此GC绝不会收集它们。•缓存功能 –任何类型的缓存机制都可以轻易导致内存泄漏。通过最终将高速缓存信息存储在内存中,它将填满并导致OutOfMemory异常。解决方案可以是定期删除较早的缓存或限制缓存量。•WPF绑定可能很危险。经验法则是始终绑定到DependencyObject或一种 INotifyPropertyChanged 宾语。如果您这样做失败,WPF将从静态变量创建对绑定源(即ViewModel)的强引用,从而导致内存泄漏。此有用的StackOverflow线程中有关WPF绑定泄漏的更多信息•被捕获的成员 –可能很明显,事件处理程序方法意味着引用了一个对象,但是当变量在匿名方法中被捕获时,也会被引用。这是内存泄漏的示例:

public class MyClass 
{ private int _wiFiChangesCounter = 0; public MyClass(WiFiManager wiFiManager) { wiFiManager.WiFiSignalChanged += (s, e) =>      _wiFiChangesCounter++; } 
}

•永不终止的线程 – 每个线程的活动堆栈都被视为GC根。这意味着在线程终止之前,GC不会收集其在堆栈上的变量的任何引用。这也包括计时器。如果您的Timer的滴答处理程序是一个方法,则该方法的对象被视为已引用,并且不会被收集。这是内存泄漏的示例:

public class MyClass   
{   public MyClass(WiFiManager wiFiManager)   {   Timer timer = new Timer(HandleTick);   timer.Change(TimeSpan.FromSeconds(5),  TimeSpan.FromSeconds(5));  }private void HandleTick(object state)  { // do something  }  
}

有关此主题的更多信息,请查看我的文章8 .NET中导致内存泄漏的方法[6]

6.使用“处置”模式来防止非托管内存泄漏

您的.NET应用程序不断使用非托管资源。

.NET框架本身在很大程度上依赖于非托管代码来进行内部操作,优化和Win32 API。

随时使用Streams,Graphics或Files 对于 例如,您可能正在执行非托管代码。使用非托管代码的.NET框架类通常实现IDisposable。那是因为非托管资源需要明确地释放,这发生在Dispose方法中。您唯一的工作就是记住并调用Dispose方法。如果可能,请使用using语句。

public void Foo()
{using (var stream = new FileStream(@"C:\Temp\SomeFile.txt",FileMode.OpenOrCreate)){// do stuff}// stream.Dispose() will be called even if an exception occurs
}

在使用语句转换的代码放到一个尝试/最后的场景,在后面声明的Dispose方法被调用的最后方法。但是,即使您不调用Dispose方法,这些资源也将被释放,因为.NET类使用Dispose Pattern[7]。这基本上意味着,如果之前未调用Dispose,则在对象被垃圾回收时从Finalizer调用它。也就是说,如果您没有内存泄漏并且确实调用了终结器。

当您自己分配非托管资源时,则绝对应该使用Dispose模式。这是一个例子:

public class MyClass : IDisposable
{private IntPtr _bufferPtr;public int BUFFER_SIZE = 1024 * 1024; // 1 MBprivate bool _disposed = false;public MyClass(){_bufferPtr =  Marshal.AllocHGlobal(BUFFER_SIZE);}protected virtual void Dispose(bool disposing){if (_disposed)return;if (disposing){// Free any other managed objects here.}// Free any unmanaged objects here.Marshal.FreeHGlobal(_bufferPtr);_disposed = true;}public void Dispose(){Dispose(true);GC.SuppressFinalize(this);}~MyClass(){Dispose(false);}
}

‍这种模式的重点是允许显式处置资源。还要增加一种保护措施,如果未调用Dispose(),则将在垃圾回收期间(在Finalizer中)处置您的资源。

该GC.SuppressFinalize(这)也很重要。如果该对象已经存在,则可以确保终结器未在垃圾回收上被调用处置。使用终结器的对象将以不同的方式释放,并且成本更高。将终结器添加到称为F-Reachable-Queue的对象中,这使该对象在额外的GC生成后仍然存在。还有其他并发症[8]

7.从代码添加内存遥测

有时,您可能想定期记录您的内存使用情况。也许您怀疑生产服务器存在内存泄漏。当您的内存达到一定限制时,您可能想采取一些措施。或者,也许您只是养成监视内存的好习惯。

我们可以从应用程序本身中获取很多信息。使用当前的内存很简单:

Process currentProc = Process.GetCurrentProcess(); 
var bytesInUse = currentProc.PrivateMemorySize64; 

有关更多信息,可以使用用于PerfMon的PerformanceCounter类:

PerformanceCounter ctr1 = new PerformanceCounter("Process", "Private Bytes", Process.GetCurrentProcess().ProcessName); 
PerformanceCounter ctr2 = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName);  
PerformanceCounter ctr3 = new PerformanceCounter(".NET CLR Memory", "# Gen 1 Collections", Process.GetCurrentProcess().ProcessName);  
PerformanceCounter ctr4 = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName);  
PerformanceCounter ctr5 = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", Process.GetCurrentProcess().ProcessName);  
//...  
Debug.WriteLine("ctr1 = " + ctr1 .NextValue());  
Debug.WriteLine("ctr2 = " + ctr2 .NextValue());  
Debug.WriteLine("ctr3 = " + ctr3 .NextValue());  
Debug.WriteLine("ctr4 = " + ctr4 .NextValue());  
Debug.WriteLine("ctr5 = " + ctr5 .NextValue());  

可从任何perfMon计数器获得信息,这是很多信息。但是,您可以更深入。CLR MD(Microsoft.Diagnostics.Runtime)允许您检查当前的内存堆并获取任何可能的信息。例如,您可以打印内存中所有已分配的类型,包括实例计数,根目录路径等。您几乎从代码中获得了一个内存探查器。

要了解使用CLR MD可以实现的目标,请查看Dudi Keleti的DumpMiner。

所有这些信息都可以记录到文件中,甚至更好地记录到遥测工具(如Application Insights)中。

8.测试内存泄漏

主动测试内存泄漏是一个好习惯。这并不难。您可以使用以下简短模式:

[Test]  
void MemoryLeakTest()  
{  var weakRef = new WeakReference(leakyObject)  // Ryn an operation with leakyObject    GC.Collect();    GC.WaitForPendingFinalizers();  GC.Collect();  Assert.IsFalse(weakRef.IsAlive); 
}

为了进行更深入的测试,诸如SciTech的.NET Memory Profiler和dotMemory之类的内存分析器提供了一个测试API:

MemAssertion.NoInstances(typeof(MyLeakyClass));    
MemAssertion.NoNewInstances(typeof(MyLeakyClass), lastSnapshot);   
MemAssertion.MaxNewInstances(typeof(Bitmap), 10);  

摘要

你的新年决心是怎样的?我新年的决心是:更好的内存管理。

我希望这篇文章能给您带来一些价值,如果您订阅[9]我的博客或在下面发表评论,我将非常乐意。欢迎任何反馈。

References

[1] 难以利用: https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z0000019S9cSAE&l=en-IL
[2] 说明: https://stackoverflow.com/a/1986486/1229063
[3] 5避免C#.NET中的事件造成内存泄漏的技术中,您应该知道: https://michaelscodingspot.com/2018/12/14/5-techniques-to-avoid-memory-leaks-by-events-in-c-net-you-should-know/
[4] 问题: https://github.com/dotnet/coreclr/issues/20156
[5] 您应该知道的5种避免C#.NET中的事件造成内存泄漏的技术: https://michaelscodingspot.com/2018/12/14/5-techniques-to-avoid-memory-leaks-by-events-in-c-net-you-should-know/
[6] 8 .NET中导致内存泄漏的方法: https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
[7] Dispose Pattern: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
[8] 其他并发症: https://www.jetbrains.com/help/dotmemory/Analyzing_GC_Roots.html
[9] 订阅: https://michaelscodingspot.com/subscribe/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASP.NET Core技术研究-探秘依赖注入框架

ASP.NET Core在底层内置了一个依赖注入框架,通过依赖注入的方式注册服务、提供服务。依赖注入不仅服务于ASP.NET Core自身,同时也是应用程序的服务提供者。毫不夸张的说,ASP.NET Core通过依赖注入实现了各种服务对象的注册和创建,…

Redis遍历方式思考--字典扩容方式

全量遍历keys 工作中线上Redis维护,有时候我们需要查询特定前缀的缓存key列表来手动处理数据。可能是修改值,删除key。那么怎么才能快速的从海量的key中查找到对应的前缀匹配项。Redis提供了一下简单的指令,例如keys用来满足特定正则下的key…

从项目到产品: 软件时代需要价值流架构师 | IDCF

译者:无敌哥原文地址: https://thenewstack.io/the-age-of-software-needs-value-stream-architects/ 本文翻译仅供学习交流之用。原文作者 Mik Kersten 出版了《Project to Product》本系列共四篇文章,分别是01 从项目到产品:软件需要从物理…

Redis高效性探索--线程IO模型,通信协议

Redis线程IO模型 Redis是单线程,这个毋庸置疑Redis单线程能做到这么高的效率?不用怀疑,还有很多其他的服务都是单线程但是也有超高的效率,比如Node.js,Nginx也是单线程。Redis单线程高效原因: Redis所有数…

Redis持久化-深入理解AOF,RDB

持久化 Redis数据全部在内存中,如果宕机,数据必然丢失,因此必须有一种机制保证Redis数据不会因为故障丢失,这就是Redis的持久化机制持久化方式两种:AOF,RDB,如下图 RDB快照模式是一次全量备份&…

推荐一个集录屏、截图、音频于一体的软件给大家

捕获屏幕,网络摄像头,音频,光标,鼠标单击和击键GitHub:https://github.com/MathewSachin/Captura特性 免费 100%免费,你不需要花一分钱开源 根据MIT许可的条款,可以在Github上获得Captura的源…

Redis高效性探索--管道

管道 开始接触Redis时候,对应Redis管道有一个错误认识,任务是redis服务器提供的一种特别的技术,有了这种技术可以加速Redis的存取效率,但是实际上Redis的管道计算(Pipeline)本身是客户端提供的技术&#x…

Redis--事务理解

事务 一个成熟的数据库系统一般都会有事务的支持,Redis作为一个缓存数据库也不例外,Redis的事务比之关系型数据库mysql,oracle等算比较简单的,Redis中无需理解那么多事务模型,可以直接使用。不过也正是因为简单&#…

.NET中的内存管理

原文来自互联网,由长沙DotNET技术社区编译。 .NET中的内存管理资源分配Microsoft .NET公共语言运行时要求从托管堆分配所有资源。当应用程序不再需要对象时,它们将自动释放。初始化进程后,运行时将保留地址空间的连续区域,该区域最…

Redis存储优化--小对象压缩

小对象压缩 Redis是一种内存数据库,内存是计算机中一种比较宝贵的资源,如果我们不注意节约,Redis很可能出现内存不足,最终导致崩溃。Redis为了优化数据结构的内存占用,增加了非常多的优化点,这些优化也是牺…

.Net微服务实战之技术架构分层篇

一拍即合上一篇《.Net微服务实战之技术选型篇》,从技术选型角度讲解了微服务实施的中间件的选择与协作,工欲善其事,必先利其器,中间件的选择是作为微服务的基础与开始,也希望给一直想在.Net入门微服务的同行有一个很好…

Redis高可用基石--主从同步

主从同步 当我们将Redis用于线上环境,单机肯定是不行的,即使不做集群,我们也应该做主从,有了主从,当主节点(master)挂掉时候,让运维将从节点(slave)接管&…

.NET 下基于动态代理的 AOP 框架实现揭秘

.NET 下基于动态代理的 AOP 框架实现揭秘Intro之前基于 Roslyn 实现了一个简单的条件解析引擎,想了解的可以看这篇文章 基于 Roslyn 实现一个简单的条件解析引擎执行过程中会根据条件的不同会在运行时创建一个类,每一次创建都会生成一个新的程序集&#…

C++实现链式基数排序

代码如下: #include <iostream> #include <cmath> using namespace std; typedef int KeyType; const int END -1; const int Radix 10;typedef struct Node {KeyType key;struct Node *next; };Node *CreateList() {KeyType x;Node *q nullptr;cin >> x…

Blazor WebAssembly 3.2.0 Preview 4 如期发布

ASP.NET团队如期3.16在官方博客发布了 Blazor WebAssembly 3.2.0 Preview 4&#xff1a;https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-preview-4-release-now-available/ &#xff0c;同时在twitter上发了一条信息带上了下面这张图&#xff0c;这张图很形象…

C#/.Net Core/WPF框架初建(国际化、主题色)

English | 简体中文作为 TerminalMACS 的一个子进程模块 - WPF管理端&#xff0c;目前搭建框架部分功能&#xff1a;本地化、国际化、主题色修改等。导航目录1.框架已添加功能说明1.1. 国际化、本地化1.2. Metro风格主窗体1.3. 动态更换主题色2.关于TerminalMACS及本WPF管理端 …

Redis底层实现--字符串

Redis字符串存储实现原理 Redis 中的字符串是可以修改的字符串&#xff0c;在内存中他是以字节数组的形式存在的。我们在入门语言C语言里面的字符串标准形式是以NULL&#xff08;即0x\0&#xff09;作为结束符&#xff0c;但是Redis里面&#xff0c;字符串表示方法不是这样&am…

[C++STL]C++实现string容器

代码如下: #pragma once #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <assert.h> #include <cstring> using namespace std;class String { public:String(const char *str ""){assert(str ! nullptr);_size strlen(str);_s…

ASP.NET Core 日志框架:Serilog

在 ASP.NET Core 日志模型 中对日志整体实现方式进行了介绍&#xff0c;通过使用内置日志记录器来实现日志的输出路径。而在实际项目开发中&#xff0c;使用第三方日志框架来记录日志也是非常多的&#xff0c;首先一般基础的内置日志记录器在第三方日志框架中都有实现&#xff…

[C++STL]C++实现vector容器

代码如下: #pragma once #include <iostream> #include <assert.h> using namespace std;template<typename T> class Vector { public:typedef T* iterator;typedef const T* const_iterator;Vector() :_start(nullptr), _finish(nullptr), _endOfStorage(…