C# 自动内存管理

一、垃圾回收

VES内置了垃圾回收支持,垃圾回收器只负责内存管理,它没有提供一个自动的系统来管理与内存无关的资源。
.NET的垃圾回收
多数CLI实现使用一个分代的(generational)、支持压缩的(compacting)、以及基于mark-and-sweep(标记并清除)的算法回收内存。
分代的:是因为只存活过短暂时间的对象与已经在垃圾回收时活下来的对象(原因是对象仍在使用)相比,前者会被更早地清理掉,这一点符合内存分配的常规模式:已经存活过较长时间的对象,会比最近才实例化的对象存活得更久一些。
.NET垃圾回收器使用了mark-and-sweep算法。在每次执行垃圾回收期间,它都会标记出将要回收的对象,并将剩余的对象压缩到一起,确保他们之间没有“脏”空间。使用压缩机制来填充由回收的对象腾出来的空间,通常会使新对象能以更快的速度实例化,这是因为不必搜索内存为一次新的分配寻找空间。
垃圾回收器会考虑机器资源以及执行时对那些资源的需求,例如,如果计算机内存尚余大量空间,垃圾回收器就很好运行,并很少花时间去清理那些资源。

二、回收过程细节

什么是垃圾,如何识别:
第一步:在一次垃圾回收周期开始的时候,它识别对象的所有根引用。根引用是来自静态变量、CPU寄存器以及局部变量或者参数实例的任何引用。基于这个根引用列表,垃圾回收可以遍历每个根引用标识的树形结构,并递归确定所有根引用指向的对象。这样,垃圾回收器就可以创建一个所有可达对象的图。

第二步:执行垃圾时,垃圾回收不是枚举所有访问不到的对象,相反,它是把所有可达对象(第一步完成的可达对象图)压缩到一起,从而覆盖不可访问的对象(也就是垃圾)所占用的内存。

第三步:为了定位和移动所有可达对象,系统要在垃圾回收器运行期间维持状态的一致性。为此,进程中所有的托管线程都会在垃圾回收期间暂停,这时就会造成应用程序出现短暂的停顿。不过,除非垃圾回收周期很长,否则,这个停顿是不太引人注意的。
为了尽量避免在不恰当的时间进行垃圾回收,System.GC对象包含一个Collect方法,可在执行关键代码(不希望被GC打断的代码)前先调用它,这样做虽然不能绝对禁止GC执行,但是会显著减小它运行的可能性---前提是关键代码执行时不会发生内存大量的消耗的情况。

分代:根据对象的生存周期的规律可以发现,相较于长期存在的对象,最近创建的对象更有可能需要被垃圾回收。所以.NET垃圾回收器提供了代(Generation)的概念,它会以更快的频率回收生存时间较短的对象。而那些已经在前一次垃圾回收中存活下来的对象则会以较低的频率清除。举例:如果把对象分为3代,那么对象初始时都是0代,当一个对象在垃圾回收周期中存活下来后,都会把它移动到下一代,直至最终移动到第2代(因为一共有3代)。相比于第二代的对象,垃圾回收器会以更快的频率对第0代对象进行GC。

三、Mark-Compact 标记压缩算法

主要处理步骤:将线程挂起→确定roots→创建reachable objects graph→对象回收→heap压缩→指针修复。

GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register) 。 Reachable objects:指根据对象引用关系,从roots出发可以到达的对象。

  简单地把.NET的GC算法看作Mark-Compact算法。阶段1: Mark-Sweep 标记清除阶段,先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;阶段2: Compact 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列,类似于磁盘空间的碎片整理。
Heap内存经过回收、压缩之后,可以继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就可以。
指针修复是因为compact过程移动了heap对象,对象地址发生变化,需要修复所有引用指针,包括stack、CPU register中的指针以及heap中其他对象的引用指针。

四、Generational 分代算法

程序可能使用几百M、几G的内存,对这样的内存区域进行GC操作成本很高,分代算法具备一定统计学基础,对GC的性能改善效果比较明显。将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉。分代算法的假设前提条件:
A、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长;
B、对部分内存进行回收比基于全部内存的回收操作要快;


五、GC的两个主要问题

首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。
第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。


六、GC注意事项

  1、只管理内存,非托管资源,如文件句柄,GDI资源,数据库连接等还需要用户去管理。

  2、循环引用,网状结构等的实现会变得简单。GC的标志-压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。

  3、GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法。

  4、GC在一个独立的线程中运行来删除不再被引用的内存。

  5、GC每次运行时会压缩托管堆。

  6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。

  7、对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数

  8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer,不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer,而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。

  9、.NET GC使用"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。

  10、GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章