.net内存管理泄漏浅析

栈:运行时初始化进程时会根据类加载器将对象放在堆内存,将静态类放在栈内存。

堆栈:进程中的每个线程都有自己单独的堆栈内存,堆栈用于存储应用程序执行过程中的静态字段、局部变量、方法参数、返回值和其他临时值。堆栈按照每个线程进行分配,并作为每个线程完成其工作的一个暂存区,垃圾收集器并不负责清理堆栈,因为为方法调用预留的堆栈会在方法返回时被自动清理。

托管堆:受运行时管理的内存空间,每个托管进程都有一个托管堆,进程中的所有线程都在同一个堆上为对象分配内存,一般存储的是引用类型。

非托管堆:不受运行时管理的内存空间,最常用的非托管资源类型是包装操作系统资源的对象,如文件句柄、窗口句柄、网络连接或数据库连接。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但无法了解如何发布并清理这些非托管资源。创建封装非托管资源的对象时,建议在公共Dispose方法中提供必要的代码以清理非托管资源。

分配内存:
初始化新进程时,运行时会为进程保留一个连续的地址空间区域, 这个保留的地址空间被称为托管堆。每个托管进程都有一个托管堆,进程中的所有线程都在同一堆上为对象分配内存。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址,最初该指针设置为指向托管堆的基址,托管堆上包含了所有引用类型,应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存,应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存,只要地址空间可用垃圾回收器就会继续以这种方式为新对象分配空间。从托管堆中分配内存要比非托管内存分配速度快,由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快,另外由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

释放内存:
垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间,垃圾回收器在执行回收时会释放应用程序不再使用的对象的内存,它通过检查应用程序的根来确定不再使用的对象,每个应用程序都有一组根,每个根或者引用托管堆中的对象或者设置为空。 应用程序的根包含线程堆栈上的静态字段、局部变量和参数以及CPU寄存器。垃圾回收器可以访问由实时(JIT)编译器和运行时维护的活动根的列表,垃圾回收器对照此列表检查应用程序的根,并在此过程中创建一个图表,在其中包含所有可从这些根中访问的对象,不在该图表中的对象将无法从应用程序的根中访问,垃圾回收器会考虑无法访问的对象垃圾,并释放为它们分配的内存。在回收中垃圾回收器检查托管堆,查找无法访问对象所占据的地址空间块,发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。在压缩了可访问对象的内存后,垃圾回收器就会做出必要的指针更正,以便应用程序的根指向新地址中的对象。它还将托管堆指针定位至最后一个可访问对象之后,请注意只有在回收发现大量的无法访问的对象时才会压缩内存,如果托管堆中的所有对象均未被回收则不需要压缩内存。为了改进性能,运行时为单独堆中的大型对象分配内存,垃圾回收器会自动释放大型对象的内存,但是为了避免移动内存中的大型对象,不会压缩此内存。

代数:
GC算法基于几个注意事项:
  1、压缩托管堆的一部分内存要比压缩整个托管堆速度快。
  2、较新的对象生存期较短,而较旧的对象生存期则较长。
  3、较新的对象趋向于相互关联,并且大致同时由应用程序访问。
垃圾回收主要在回收短生存期对象时发生。为优化垃圾回收器的性能,将托管堆分为三代:第0代、第1代和第2代,因此它可以单独处理长生存期和短生存期对象。 垃圾回收器将新对象存储在第0代中,在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第1级和第2级中。 因为压缩托管堆的一部分要比压缩整个托管堆速度快,所以此方案允许垃圾回收器在每次执行回收时释放特定级别的内存而不是整个托管堆的内存。

第0代:
  这是最年轻的代,其中包含短生存期对象。短生存期对象的一个示例是临时变量,垃圾回收最常发生在此代中,新分配的对象构成新一代对象,并隐式地成为第0代集合,但是如果它们是大型对象,它们将延续到大型对象堆(LOH),这有时称为第3代。第3代是在第2代中逻辑收集的物理生成,大多数对象通过第0代中的垃圾回收进行回收,不会保留到下一代。如果应用程序在第0代托管堆已满时尝试创建新对象,垃圾回收器将执行收集,以尝试为该对象释放地址空间,垃圾回收器从检查第0 级托管堆中的对象(而不是托管堆中的所有对象)开始执行回收,单独回收第0代托管堆通常可以回收足够的内存,这样应用程序便可以继续创建新对象。

第1代:
  这一代包含短生存期对象并用作短生存期对象和长生存期对象之间的缓冲区。垃圾回收器执行第0代托管堆的回收后,会压缩可访问对象的内存,并将其升级到第1代,因为未被回收的对象往往具有较长的生存期,所以将它们升级至更高的级别很有意义。垃圾回收器不必在每次执行第0代托管堆的回收时,都重新检查第1代和第2代托管堆中的对象。如果第0代托管堆的回收没有回收足够的内存供应用程序创建新对象,垃圾回收器就会先执行第1代托管堆的回收,然后再执行第2代托管堆的回收,第1级托管堆中未被回收的对象将会升级至第2级托管堆。

第2代:
  这一代包含长生存期对象。长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。第2代托管堆中未被回收的对象会继续保留在第2代托管堆中,直到在将来的回收中确定它们无法访问为止,大型对象堆上的对象(有时称为第3代)也在第2代中收集,当条件得到满足时,垃圾回收将在特定代上发生,回收某个代意味着回收此代中的对象及其所有更年轻的代。第2代垃圾回收也称为完整垃圾回收,因为它回收所有代中的对象(即托管堆中的所有对象)。

内存泄漏:
在存在垃圾收集器(GC)的情况下,内存泄漏表示有些对象仍在引用中,但实际上未被使用,由于已引用它们,因此GC将不会收集它们,并且它们将永久保存占用内存。

弱引用:我们平常用的都是对象的强引用,如果有强引用存在GC是不会回收对象的,我们能不能同时保持对对象的引用,而又可以让GC需要的时候回收这个对象呢?.NET中提供了WeakReference来实现。弱引用可以让您保持对对象的引用,同时允许GC在必要时释放对象回收内存,对于那些创建便宜但耗费大量内存的对象,即希望保持该对象,又要在应用程序需要时使用,同时希望GC必要时回收时,可以考虑使用弱引用。了解弱引用之前,先了解一下什么是强引用,例如:Object obj=new Object(); 就是一个强引用,内存分配一份空间给用以存储Object数据,这块内存有一个首地址,也就是obj所保存的数据,内存分配的空间中不仅仅保存着Object对象信息,还保存着自己(Object本身)被引用的次数。当一个对象被强引用的形式创建的时候,本身被引用的次数已经为1。接着Object o=obj; 这句代码执行之后,obj指向的Object的存储空间已经被引用了2次,所以Object保存的被引用数值为2,总结:强引用最终导致的结果就是被引用的对象的被引用次数+1,相反的弱引用就是不会对被引用对象的被引用次数有任何影响。

检测内存泄漏的工具:JetBrains dotMemory(第三方工具),VS自带的诊断工具,利用终结器(就是析构函数)来测试。

VS自带诊断工具使用说明:

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

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

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

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

终结器工具:

获取内存信息的方法:PerfMon、MemAssertion类。

主动测试内存泄漏:

内存泄漏案例及规避方案:

1、.NET中的事件导致内存泄漏:

在此示例中我们假设WifiManager在程序的整个生命周期中都处于活动状态,执行Main之后将创建MyClass的实例并且不再使用它,程序员可能会认为GC将收集它但事实并非如此,因为MyClass.OnWifiChanged被WifiManager.WifiSignalChanged引用,所以MyClass类被引用不会被GC回收。

怎样解决注册事件导致的内存泄漏呢?可以适时注销事件即可:

在某些情况下,您可能希望事件处理程序仅发生一次,可以使用下面2种方法注销事件:

或者:

使用弱事件(因为弱引用的对象没有被引用次数累加的说法,所以可以被GC回收):

弱事件使用说明:
实施“弱事件”模式的系统通常会宣传它们正在解决与经典事件相关的内存泄漏问题,这样的系统可能还有其他合法的优势,但是他们解决内存泄漏问题的尝试是一把双刃剑,尽管确实如此,但在订户忽略取消订阅的情况下,这种系统实际上可以防止内存泄漏,但它们也将为因僵尸调用事件处理程序而导致的细微错误打开大门,在这种情况下,治愈可能比疾病还差,不要相信鼓励您使用弱事件模式的陈述会导致您忽略取消订阅事件,总是找到在某个适当时间取消订阅事件的方法。这并不意味着我不建议使用弱事件管理器,就像我之前提到的,它们在其他方面与经典事件有所不同,这些差异在某些情况下可能是有益的,无论如何,在弱事件没有经典事件明显优势的情况下,最好使用经典事件,如果您决定使用弱事件,请确保您不要忽略取消订阅事件(消息)。

 2、.net中的静态变量导致的内存泄漏,请记住所有静态变量都是GC根,因此GC绝不会收集它们:

  

 3、缓存功能导致内存泄漏,如果无限期地缓存最终将耗尽内存,解决方案可以是定期删除较早的缓存或限制缓存量:

 

 4、wpf不正确的绑定导致内存泄漏,经验法则是始终绑定到DependencyObject或INotifyPropertyChanged,否则WPF将从静态变量创建对绑定源(即ViewModel)的强引用,从而导致内存泄漏。

 

 内存泄漏的写法如下:

正确的写法如下:

解释说明:是否调用PropertyChanged实际上并不重要,重要的是该类是从INotifyPropertyChanged派生的,因为这会告诉WPF不要创建强引用。另一个和WPF有关的内存泄漏问题会发生在绑定到集合时,如果该集合未实现INotifyCollectionChanged接口,则会发生内存泄漏,你可以通过使用实现该接口的ObservableCollection来避免此问题。

5、远运行的不执行任何操作并且具有对对象引用的线程导致内存泄漏,每个线程的活动堆栈都被视为GC根,这意味着在线程终止之前,GC不会收集其在堆栈上的变量的任何引用,这也包括计时器。实时堆栈会被视为GC的根,实时堆栈包括正在运行的线程中的所有局部变量和调用堆栈的成员,如果出于某种原因,你要创建一个永远运行的不执行任何操作并且具有对对象引用的线程,那么这将会导致内存泄漏。对比System.Timers.Timer和System.Threading.Timer理解定时器线程保活机制:

运行结果如下(Foo实例没有被回收,定时器仍在运行):

将timer_Elapsed()改为静态方法(静态方法存在堆栈是GC的根没有被任何对象引用的说法):

运行结果如下(Foo实例被回收,但是定时器仍在运行,System.Timers.Timer保活机制,即使它所属的实例已被回收):

将定时器换成System.Threading.Timer:

运行结果如下:

解释说明:.NET Framework会确保System.Timers.Timer的存活,即便其所属实例已经被销毁回收。.NET Framework不会保存激活System.Threading.Timer的引用,而是直接引用回调委托。

6、匿名方法中捕获类成员导致内存泄漏:

这里匿名委托引用了MyClass的成员变量_wiFiChangesCounter,从而导致MyClass被wifiManager引用,导致内存泄漏。解决办法如下:

7、非托管内存泄漏:

在上述方法中,我们使用了Marshal.AllocHGlobal方法,它分配了非托管内存缓冲区。在这背后AllocHGlobal会调用Kernel32.dll中的LocalAlloc函数,如果没有使用Marshal.FreeHGlobal显式地释放句柄,则该缓冲区内存将被视为占用了进程的内存堆,从而导致内存泄漏。使用IDisposable接口的Dispose()方法来解决这个问题,其实.net提供的语法糖using大家也可以尝试使用,IDisposable解决示例如下:

原文地址:https://www.cnblogs.com/happyShare/p/14163616.html