.Net 自动内存管理

.Net 自动内存管理,第1张

.Net依赖CLR(公共语言运行时)实现自动内存管理,标准CLR使用分代式标记-压缩GC对托管堆上对象进行自动内存管理。

1、GC过程

(1)标记阶段:从应用程序的根找到所有可达对象进行标记并创建一个对象引用图;

每个应用程序都有一组根,应用程序的根包含线程堆栈上的静态字段、局部变量和参数以及CPU寄存器。垃圾回收器可以访问由实时编译器(JIT)和运行时维护的活动根的列表。

(2)清除阶段:

a、未标记对象如果没有终结器(析构函数)则立即回收;

b、有终结器对象在创建时会将对象引用放到终结队列,当此对象变为垃圾时,垃圾收集器会将其引用从终结队列移到f-reachable队列。GC完成后一个终结器线程会遍历f-reachable队列,获取每个对象执行其Finalize方法,待该对象的Finalize方法执行完后,将该对象引用从f-reachable队列移除。这些对象会在下一次GC中回收(除非该对象复活);

如果某个终结器对象在终结器过程中复活(Resurrection),但是复活时不会将对象的引用重新放到终结队列,如果想让复活对象下次回收时执行Finalize方法,可以在对象复活时手动调用GC.ReRegisterForFinalize(obj)将该对象的引用添加到终结队列。

另外GC.SuppressFinalize(Object obj)可以将对象的引用从终结队列移除,垃圾回收时不再执行Finalize方法。一般SuppressFinalize用于同时实现了Finalize和Dispose方法来释放资源的情况下,在Dispose方法中调用GC.SuppressFinalize(this)。Finalize方法作为忘记调用Dispose释放资源的一个保障。

(3)压缩阶段:清除完后将所有存活对象移到堆的起始位置。压缩可以防止碎片化,同时避免耗时的空内存片段列表维护,直接用简单的策略将堆的尾部内存分配给新对象。

2、GC触发时机:

(1)由托管堆上已分配对象的使用内存超过特定阈值(该阈值可能随着进程的运行不断调整)时;

(2)调用System.GC.Collect手动触发;

(3)系统具有低的物理内存。通过OS的内存不足通知或主机指示的内存不足检测出来。

3、CLR中GC优化

3.1、分代回收

垃圾回收器将堆上内存对象分为3代:

第0代:新分配对象及从未经过垃圾回收的对象集合。通常仅有几百KB到几MB;

第1代:第0代回收中存活的对象集合;

第2代:第1代和第2代中未回收的对象集合。

因此可以单独处理长生存期和短生存期对象。每一代都维护一个阀值,当第n代内存达到该阀值时会触发对第n代的收集,当垃圾回收器检测到某个代中的幸存率很高时,会增加该代的分配阈值。回收一代时同时回收它前面的所有代。

3.2 、大对象堆(Large Object Heap,LOH)

加载CLR时,GC分配两个初始堆段:一个用于小型对象(小对象堆SOH),一个用于大型对象(大对象堆 LOH)。垃圾回收器将大于等于一个阈值(目前是85000字节)的对象分配到大对象堆。大对象堆上对象都按第2代处理,可以避免过量的第0代回收。由于复制大型对象代价太大,默认不压缩大对象堆,所以需要维护空闲内存块链表,并会产生碎片化问题。在.Net Core和.Net Framework 4.5.1以上版本中,可以使用 GCSettings.LargeObjectHeapCompactionMode  属性按需压缩大对象堆。

3.3、并发和后台回收

(1)并发垃圾回收

仅适用于工作站垃圾回收的.Net Framework 3.5及更早版本;服务器垃圾回收.Net Framework 4及更早版本。更高版本中,后台垃圾回收取代了并发垃圾回收。

并发垃圾回收只影响第2代垃圾回收,第0代和第1代的垃圾回收始终是非并发的。并发垃圾会输在一个专用线程上执行,运行并发垃圾回收线程的大多数时间,托管线程可以继续运行,最大程度减少因回收引起的暂停。

(2)后台垃圾回收

在.Net Framework 4 及更高版本中,后台垃圾回收替换并发垃圾回收。但在.Net Framework 4 中仅支持工作站垃圾回收,.Net Framework 4.5开始,后台垃圾回收可用于工作站和服务器垃圾回收。

后台垃圾回收只适用于第2代回收,后台垃圾回收在一个或多个专用线程上执行行,后台垃圾回收进行中,后台垃圾回收线程将在常见的安全点上检查,如果发现第0代或第1代空间不足需要前台垃圾回收时,后台垃圾回收会暂停自己并让前台垃圾回收(对暂时代(第0代和第1代)的回收)执行。前台垃圾回收完成之后,后台回收线程和用户线程将继续。

3.4、工作站和服务器垃圾回收

CLR提供以下类型的垃圾回收,可以基于工作负载的特征设置垃圾回收类型:

(1)工作站垃圾回收:为客户端应用设计;

回收发生在触发垃圾回收的用户线程上,并保留与用户线程相同的优先级(普通优先级),所以垃圾回收线程必须与其它线程竞争CPU时间。

(2)服务器垃圾回收:用于需要高吞吐量和可伸缩性的服务器应用程序

回收发生在以 THREAD_PRIORITY_HIGHEST 优先级运行的多个专用线程上,为每个CPU提供一个用于执行垃圾回收的一个堆和专用线程,多个垃圾回收线程一起工作。

3.4、垃圾回收通知(为担负大量请求的服务器应用准备)

服务器版本的CLR可以在完全垃圾回收之前发送通知。可以调用GC.RegisterForFullGCNotification启用通知。然后开启另一个线程持续监听GC.WaitForFullGCApproach(),当返回的GCNotificationStatus表示即将进行一次回收时,将工作负载重定向到另一个服务器实例,然后监听GC.WaitForFullGCComplete(),当该方法返回的状态表明回收完毕时在重新开始接受请求。

4、弱引用

CLR由System.WeakReference类实现弱引用,将Target属性设置为该对象。当垃圾收集器遇到一个弱引用指针指向对象时,不会将该对象加入引用关系图中。如果一个对象只有弱引用指向它,该对象不会被标记,垃圾回收时就可以清除此对象。

(1)短弱引用

垃圾回收回收对象后,弱引用的Target会变为null。弱引用本身时托管对象,也需要经过垃圾回收。

(2)长弱引用

在对象的Finalize方法调用后,长弱引用获得保留。这样便可以重新创建新对象。

若要建立强引用,可以将WeakReference的Target属性(前提是Target属性不为null,即对象未被回收)强制转换为对象类型。

在分析内存分配时 应该先了解关于堆栈的区别

堆的分配向高地址扩展 而栈的分配向低地址扩展

一 内存分配

关于内存的分配 首先应该了解分配在哪里的问题 CLR管理内存的区域 主要有三块 分别为

· 线程的堆栈 用于分配值类型实例 堆栈主要由 *** 作系统管理 而不受垃圾收集器的控制 当值类型实例所在方法结束时 其存储单位自动释放 栈的执行效率高 但存储容量有限

· GC堆 用于分配小对象实例 如果引用类型对象的实例大小小于 字节 实例将被分配在GC堆上 当有内存分配或者回收时 垃圾收集器可能会对GC堆进行压缩 详情见后文讲述

public   class  VIPUser:User       {          //分配 Byte           public   bool  isVip          public   bool  IsVipUser()          {               return  isVip         }            static   void  Main( string [] args)           {               //分配内存空间和初始化 *** 作               VIPUser aUser              //将对象引用赋给aUser变量 建立aUser和VIPUser的关联               aUser =  new  VIPUser()              //Q:类型的分配的字节数?               //就本类而言需要 Byte 但是实例对象所占的字节总数还要加上对象附加成员所需的字节数 其中包括附加成员TypeHandle和SyncBlockIndex共 个字节 在托管堆上分配的字节总数为 字节 而堆上的内存块总是按照 Byte的倍数进行分配 因此本类中将分配 字节的地址空间                  //最后调用对象构造器 进行对象初始化 *** 作 完成创建                  //构造过程               //a 构造VIPUser类型的Type对象 主要包括静态字段 方法表 实现的接口等 并将其分配在上文提到托管堆的Loader Heap上                  //b 初始化aUser的两个附加成员 TypeHandle和SyncBlockIndex 将TypeHandle指针指向Loader Heap上的MethodTable CLR将根据TypeHandle来定位具体的Type 将SyncBlockIndex指针指向Synchronization Block的内存块 用于在多线程环境下对实例对象的同步 *** 作                  //c 调用VIPUser的构造器 进行实例字段的初始化 实例初始化时 会首先向上递归执行父类初始化 直到完成System Object类型的初始化 然后再返回执行子类的初始化 直到执行VIPUser类为止 以本例而言 初始化过程为首先执行System Object类 再执行User类 最后才是VIPUser类 最终 newobj分配的托管堆的内存地址 被传递给VIPUser的this参数 并将其引用传给栈上声明的aUser                  aUser isVip =  true               Console WriteLine(aUser IsVipUser())              //上述过程 基本完成了一个引用类型创建 内存分配和初始化的整个流程           }       }        public   class  UserInfo       {           //分配 个字节            private  Int  age =            //分配 个字节            private   char  level =  A       }        public   class  User       {           //分配 byte            private  Int  id          //保存了UserInfo的引用 占用 Byte           //仅是一个引用(指针) 保存在线程的堆栈上 占用 Byte的内存空间 用于保存user对象的有效地址 现在试图对user的任何 *** 作将抛出NullReferenceException            private  UserInfo user      } 

LOH(Large Object Heap)堆 用于分配大对象实例 如果引用类型对象的实例大小不小于 字节时 该实例将被分配到LOH堆上 而LOH堆不会被压缩 而且只在完全GC回收时被回收

在了解内存分配之前  首先了解一下三个概念

TypeHandle 类型句柄 指向对应实例的方法表 每个对象创建时都包含该附加成员 并且占用 个字节的内存空间 我们知道 每个类型都对应于一个方法表 方法表创建于编译时 主要包含了类型的特征信息 实现的接口数目 方法表的slot数目等

SyncBlockIndex 用于线程同步 每个对象创建时也包含该附加成员 它指向一块被称为Synchronization Block的内存块 用于管理对象同步 同样占用 个字节的内存空间

NextObjPtr 由托管堆维护的一个指针 用于标识下一个新建对象分配时在托管堆中所处的位置 CLR初始化时 NextObjPtr位于托管堆的基地址

二 继承本质论

//Bird bird创建的是一个对象的引用 而new Bird()是创建Bird对象 分配内存和初始化 *** 作 然后将对象引用赋给bird变量 也就是简历bird和Bird 之间的关联  Bird bird =  new  Bird() // 从继承的角度来分析CLR在运行时如何执行对象的创建过程  //  首先是字段的创建 字段的存储顺序由上到下排列 最高层类的字段排在最前面  // 方法表的创建是类第一次加载到AppDomain时完成的 在对象创建时只是将其附加成员TypeHandle指向方法列表Loader Heap上的地址 将对象与其动态方法列表相关联起来 因此方法表示先于对象存在的     Chicken ch =  new  Chicken()  lishixinzhi/Article/program/net/201311/13074

尽管很多人相信在.net应用中谈及内存及资源泄露是件很轻松的事情。但GC(垃圾回收器)并不是魔法师,并不能把你完全从小心翼翼处理内存与资源损耗中解放出来。

本文中我将解释缘何内存泄露依然存在以及如何避免其出现。别担心,本文不涉及GC内部工作机制及其它.net的资源及内存管理等高级特性中。

理解泄露本身及如何避免其出现很重要,尤其因为它无法轻松地自动检测到。单元测试在此方面无能为力。一旦产品中你的程序崩溃了,你需要马上找出解决方案。所以在一切都还不是太晚前,花些时间来学习一下本文吧。

Table of Content

· 介绍

· 泄露?资源?指什么?

· 如何检测泄露并找到泄露的资源

· 常见内存泄露原因

· 常见内存泄露原因演示

· 如何避免泄露

· 相关工具

· 结论

· 资源

介绍

近期,我参与了一个大的.net项目(暂叫它项目X吧),我在项目中负责追踪内存与资源泄露。大部分时间我都花在与GUI关联的泄露上,更准确地说是一个基于Composite UI Application Block (CAB).的windows窗体应用。接下来我要说的直接应用到winform上的内容,多数见解同样可以适用到其它.net应用中(像WPF,Silverlight,ASP.NET,Windows service,console application 等等)。

我不是个处理泄露方面的专家,所以我不得不深入钻研了一下应用程序,做一些清理工作。本文的目标是与你们分享在我解决问题过程中的所得所悟。希望能够帮助那些需要检测与解决内存、资源泄露问题的朋友。下面的概述部分首先会介绍什么是泄露,之后会看看如何检测到泄露和被泄露资源,以及如何解决与避免类似泄露,最后我会列出一个对此过程有帮助的工具列表及相关资源。

泄露?资源?指什么?

内存泄露

在进一步深入前,让我们先来定义下我所谓的“内存泄露”。简单引用在Wikipedia上找到的定义吧。该定义与我打算通过本文所帮助解决的问题完美的一致:

在计算机科学领域中,内存泄露是指一种特定的内存损耗,该损耗是由一个计算机程序未成功释放不需要的内存引起的。通常是程序中的BUG阻碍了不需要内存的释放。

仍然来自Wikipedia:”以下语言提供了自动的内存管理,但并不能避免内存泄露。像 Java,C#,VB.NET或是LISP等。”

GC只回收那些不再使用的内存。而使用中的内存无法释放。在.net中,只要有一个引用指向的对象均不会被GC所释放。

句柄与资源

内存可不是唯一被视为资源的。当你的.net应用程序在Windows上运行时,消耗着一个完整的系统资源集。微软定义了系统三类对象:用户(user),图形设备接口(GUI),以及系统内核(kernel)。我不会在此给出完整的分类对象列表,只是指出一些重要的:

· 系统通过使用用户对象(User objects) 来支持windows管理。相关对象包括:提速缓冲表(Accelerator tables),Carets(补字号?),指针(Cursors),钩子(Hooks),图标(Icons),菜单(Menus)和窗体(Windows)。

· GDI对象 支持图形绘制:位图(bitmaps),笔刷(Brushes),设备上下文(DC),字体(Fonts),内存设置上下文(Memory DCs),元文件(Metafiles),画笔(Pens),区域(Regions)等。

· 内核对象 支持内存管理,进程执行和进程间通讯(IPC):文件,进程,线程,信号(Semaphores),定时器(Timer),访问记号(Access tokens),套接字(Sockets)等。

所有系统对象的详细情况都可以在MSDN中找到。

系统对象之外,你还会碰到句柄(handles).据MSDN的陈述,应用程序不能直接访问对象数据或是对象所代表的系统资源。取而代之,应用程序一定都会获得一个对象句柄(Handle),可以使用它检查或是修改系统资源。在.net中无论如何,多数情况下系统资源的使用都是透明的,因为系统对象与句柄都由.net类直接或间接代表了。

非托管资源

像系统对象(System objects)这样的资源自身都不是个问题,但本文仍涵盖了它们,因为像Windows这样的 *** 作系统对可同时打开的 套接字、文件等的数量都有限制。所以关注应用程序所使用系统对象的数量非常重要。

在特定时间段内一个进程所能使用的User与GDI对象数目也是有配额的。缺省值是10000个GDI对象和10000个User对象。如果想知道本机的相关设置值,可以使用如下的注册表键:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota 和 USERProcessHandleQuota.

猜到了什么?确实没有这么简单,还有一些你会很快达到的其它限制。比如参照:我的一篇有关桌面堆的博客 所述。

假设这些值是可以自定义的,你也许认为一个解决方案就是打破默认值的限制—调高这些配额。但我认为这可不是个好主意,有如下原因:

1. 配额存在的原因:系统中不是只有你独自一个应用程序,所有运行在计算机中的其它进程与你的应用应该分享系统资源。

2. 如果你修改配额,使它不同于其它系统了。你不得不确认所有你的应用程序需要运行的机器都完成了这样的修改,而且这样的修改从系统管理员的角度来说是否会有问题也需要确认。

3. 大部分都采用了默认配额值。如果你发现配置值对你应用程序来说不够,那你可能确实有些清理工作要做了。

如何检测泄露及找到泄露的资源

泄露带来的实际问题在MSDN上的一篇文章中有着很好的描述:

哪怕在小的泄露只要它反复出现也会拖垮系统。

这与水的泄露异曲同工。一滴水的落下不是什么大问题。但是一滴一滴如此反复的泄露也会变为一个大问题。

像我稍后解释的,一个无意义的对象可以在内存中维持一整图的重量级对象。

仍然是同一篇文章,你会了解到:

通常三步根除泄露:

1.发现泄露

2.找到被泄露的资源

3.决定在源码中何时何处释放该资源

最直接“发现”泄露的方式是遭受泄露引发的问题

你或许没有见过内存不足。“内存不足”提示信息极少出现。因为 *** 作系统运行中实际内存(RAM)不足时,它会使用硬盘空间来扩展内存。(称为虚拟内存)。

在你的图形应用程序中可能更多出现的是“句柄不足”的异常。准确的异常不是System.ComponentModel.Win32Exception 就是 System.OutOfMemoryException 均包含如下信息:”创建窗体句柄错误”。这两个异常多发于两个资源被同时使用的情况下,通常都因为该释放的对象没有被释放所致。

另外一种你会经常碰到的情况是你的应用程序或是整个系统变更得越来越慢。这种情况的发生是因为你的系统资源即将耗尽。

我来做个生硬的推断:大多数应用程序的泄露在多数时间里都不是个问题,因为由泄露导致出现的问题只在你的应用程序集中使用很长时间的情况下才会出现。

如果你怀疑有些对象在应该被释放后仍逗留在内存中,那需要做的第一件事就是找出这些对象都是什么。

这看起来很明显,但是找起来却不是这样。

建议通过内存工具找到非预期逗留在内存中的高级别对象或是根容器。在项目x中,这些对象可能是类似LayoutView实例一样的对象们(我们使用了MVP(Model View Presentation )模式)。在你的实际项目中,它可能依赖于你的根对象是什么。

下一步就是找出它们该消失却还在的原因。这才是调试器与工具能真正帮忙的。它们可以显示出这些对象是如何链接在一起的。通过查看那些指向“僵尸对象”(the zombie object)的引用你就可以找到引起问题的根本原因了。

你可以选择 ninja方式(译者:间谍方式?)(参照 工具介绍章节中有关 SOS.dll 和 WinDbg 的部分)。

我在项目X中用了JetBrains的dotTrace,本文中我将继续使用它来介绍。在后面的工具相关章节中我会向你更多的介绍该工具。

你的目标是找到最终引起问题的那个引用。不要停留在你找到的第一个目标上,但是也要问问自己为什么这个家伙还在内存中。

常见内存泄露的原因

上面提到的泄露情况在.net中较常见。好消息是造成这些泄露的原因并不多。这意味着当你尝试解决一个泄露问题时,不需要在大量可能的原因间搜寻。

我们来回顾一下这些常见的罪魁祸首,我把它们区别开来:

· 静态引用

· 未注销的事件绑定

· 未注销的静态事件绑定

· 未调用Dispose方法

· Dispose方法未正常完成

除了上列典型的原因外,还有些其它情况也可能引发泄露:

· Windows Forms:绑定源滥用

· CAB:未移除对工作项的调用

我只列出了可能在你应用程序中出现的一些原因,但应该清楚你的应用程序依赖的其它.net代码、库实际使用中也可能引发泄露。

我们来举个例子。在项目x中,使用了一套第三方控件来构造界面。其中一个用来显示所有工具栏的控件,它管理着一个工具栏列表。这种方式没什么,但有一点,即使被管理的工具栏自身实现了IDisposable接口,管理类却永远也不会去调用它的Dispose方法。这是一个bug.幸运的是这发生在一个很容易发现的工作区:只能我们自身来调用所有工具样的Dispose方法了。不幸的是这还不够,工具栏类自身问题也不少:它并没有释放自身承载的控件(按钮,标签等等)。所以在解决方案中还要添加对每个工具栏中控件的释放,但是这次可就没那么简单了,因为工具栏中的每个子控件都不同。不管怎么样这只是一个特殊的例子,我要表达的观点是你应用程序中使用的任何第三方库、组件都可能引发泄漏。

最后,还有一种由.net framework造成的泄露,由一些不好的使用习惯引起。即使.net framework自身可能引发泄露,但这是你极少会遭遇到的情况。把责任推到.net身上很容易,但在我们把问题推到别人头上前,还是应该先从自身写的代码出发,看看里面有没有问题。

常见泄露演示

我已经列举出了泄露主要的来源,但我还不想仅限于此。如果每个泄露我都能举个鲜活的例子的话,我想本文会更实用些。好,我们先启动Vs 和 dotTrace , 然后看些示例代码。我会同时演示如何解决或是避免每个泄露情况。

项目X中使用了CAB和MVP模式,这意味着界面由工作空间、视图和呈现者组成。简单起见,我决定使用包含一组窗口的Winform应用。其中使用了与Jossef Goldberg的一篇关于“Wpf应用程序内存泄露”文章中相同的方法。甚至我会直接把相同的例子和事件处理函数应用到我的Winform App中。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/tougao/11192840.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-14
下一篇 2023-05-14

发表评论

登录后才能评论

评论列表(0条)

保存