扩大
缩小

.NET GC垃圾回收器

GC垃圾回收器简介

全名:

Garbage Collector

原理:

以应用程序的根(root)为基础,遍历应用程序堆(heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的,哪些仍需要被使用,已经不再被应用程序的根(root)或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。

Mark Compact(标记压缩算法)

原理:

  1. 先假设Heap中的所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后Heap中没有打标记的对象都是可以被回收的。
  2. 对象回收之后,Heap内的存储空间变得不连续,在Heap中移动这些对象,使他们重新从Heap的基地址开始连续排列。

步骤:

将线程挂起 -> 确定Roots -> 创建Reachable Object Graph(可达的对象图) -> 对象回收 -> Heap压缩 -> 指针修复

Roots:

Heap中对象的引用关系错综复杂(交叉引用,循环引用),形成复杂的Graph(图),Roots是CLR在Heap之外可以找到的各种入口点。

Reachable Objects:

指根据对象引用关系,从Roots出发可以到达的对象。例如当前执行函数的局部变量对象A是一个Root object,他的成员变量引用了对象B,则B是一个Reachable object。从roots出发可以创建Reachable objects graph,剩余对象即为unreachable,可以被回收。

GC搜索Roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针、终止队列(Finalization Queue)等。主要可归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(Stack + CPU Register)。

Debug和Release执行模式之间稍有区别,Release模式下后续代码没有引用的对象是Unreachable的,而debug模式下需要等到当前函数执行完毕,这些对象才会成为Unreachable,目的是为了调试时跟踪局部对象的内容。传给了COM+的托管对象也会成为root,并且具有一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时,这些对象才可能成为被回收对象。Pinned objects指分配之后不能移动位置的对象,例如传递给非托管代码的对象(或者使用了fixed关键字),GC在指针修复时无法修改非托管代码中的引用指针,因此将这些对象移动将发生异常。pinned objects会导致heap出现碎片,但大部分情况来说传给非托管代码的对象应当在GC时能够被回收掉。

Generational(分代算法) 

原理:

将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉。

使用条件:

  1. 大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长
  2. 对部分内存进行回收比基于全部内存的回收操作要快
  3. 新创建的对象之间关联程度通常较强。Heap分配的对象是连续的,关联度较强有利于提高CPU Cache(快速缓冲贮存区)的命中率

.NET将Heap分成3个代龄区域: Gen 0、Gen 1、Gen 2,相应的GC有3种方式: #Gen 0 Collections, # Gen 1 Collections, #Gen 2 Collections。如果Gen 0 Heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 Heap和Gen 1 Heap一起进行回收,幸存的对象进入Gen2。

2代GC将Gen 0 Heap、Gen 1 Heap和Gen 2 Heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为Full GC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,Full GC可能需要花费几秒时间。大致上来讲.NET应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。

Finalization Queue(终结队列)和Freachable Queue(可达队列)

这两个队列和.NET对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。

在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。

.NET Framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生。

结构:分代算法中的GC回收器使用的是标记压缩算法,标记压缩算法在Mark的时候会使用Finalization Queue和Freachable Queue。

大对象回收:

一个托管堆(managed heap)是垃圾回收器从操作系统申请的内存区(通过调用windows api VirtualAlloc)。当CLR载入内存之后,会初始化两个托管堆,一个大对象堆(LOH –large object heap)和一个小对象对(SOH – small object heap)。

在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象。这个数字是根据性能优化的经验得到的。

大对象回收是和二代一起回收的,大对象回收后不会进行移动(碎片整理),而是将空闲的内存当作下一次一个比空闲内存小于的对象放进去,此操作会导致出现内存碎片,并且这个碎片就在这个程序的生命周期中永远不能被再次利用了。

如果大对象堆上没有足够的空闲内存容纳要申请的大对象空间,CLR首先会尝试向操作系统申请内存,如果申请失败,就会触发一次二代回收来尝试释放一些内存。

在2代垃圾回收时,可以将不需要的内存通过VirtualFree交还给操作系统。

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代对象,从而在内存中停留更长时间。
posted @ 2019-08-15 08:35  戈多编程  阅读(256)  评论(0编辑  收藏  举报