垃圾回收笔记

1. 为什么会有自动垃圾回收(garbage collection)?

非托管编程时,内存管理是一件很头疼的事情。首先,如果程序员忘记释放不再需要的内存,或者试图使用已被释放的内存,会造成无法预测的后果;其次,正确进行资源管理通常很难而且很枯燥,它会极大分散开发人员的注意力,使之无法专注于真正要解决的问题。

2. 垃圾回收简介

垃圾回收使开发人员得到了解放,程序员不必再考虑内存管理,这一切交给垃圾回收器完成。每次使用new运算符(对应的是 IL的newobj指令)创建对象时,运行库都从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行库就会继续为对象分配空间。但是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。

需要注意的是:值类型、集合类型、String、Attribute、Delegate和Exception所代表的资源无需执行特殊的清理工作。

3. 托管堆的内存分配和NextObjPtr

当进程初始化时,CLR就保留一块连续的地址空间,这个地址空间最初并没有对应的物理存储,这个地址空间就是托管堆。托管堆中维护着一个指针,叫做NextObjPtr,它指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr设为保留地址空间的基地址。当IL指令newobj创建对象,CLR会执行以下步骤:

(1) 计算类型(及所有积累性)的字段需要的字节数

(2) 加上对象的开销所需要的字节数

(3) 如果在托管堆中有足够的可用空间,对象会被放入NextObjPtr指针指向的地址处。接着会调用类型的实例构造器并返回对象的内存地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样会得到一个新值,它指向下一个对象放入托管堆时的地址

4. 垃圾回收算法

垃圾回收器检查托管堆中是否有应用程序不再使用的任何对象。如果有,它们使用的内存就可以回收(否则抛出OutOfMemoryException)。

每个应用程序都包含一组root)。每个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null。例如,类型中定义的静态字段被认为是一个根,任何方法参数或局部变量也被认为是一个根。只有引用类型的变量才被认为是根;值类型永远不认为是根。

垃圾回收开始执行时,它假设堆中所有对象都是垃圾。垃圾回收器的第一阶段是所谓的标记(marking)阶段。在这个阶段中,垃圾回收器遍历所有线程栈以检查所有根(由于要遍历所有的线程栈,所以减少线程数可显著提升垃圾回收性能)。如果根应用了一个对象,就会在对象的“同步块索引字段”上开启一位-----对象就是这样“标记”的。

检查好所有的根之后,堆中将包含一组已标记和未标记的对象。已标记的对象是通过应用程序的代码可达的对象,而未标记的对象是不可达的。不可达的对象被认为是垃圾,它们占用的内存可以被回收。现在,垃圾回收器开始第二个阶段,即压缩(compact)阶段。在这个阶段中,垃圾回收器线性遍历堆,以寻找未标记(垃圾)对象的连续内存块。如果内存块比较小,垃圾回收器会忽略它们。但是,如果发现大的、可用的连续内存块,垃圾回收器会把非垃圾的对象移动到这里以压缩。很自然,移动内存中的对象之后,包含“指向这些对象的指针”的变量和cpu寄存器现在变得无效。所以,垃圾回收器必须重新访问应用程序的所有根,并修改它们来指向对象的新的内存位置。

5. 垃圾回收与调试

6. 使用终结(Finalize)操作来释放本地资源

终结(finalization)是CLR提供的一种机制,允许对象在垃圾回收器回收其内存之前执行一些得体的清理工作。可以这样理解:实现了Finalize方法的任何类型实际上是在说,它的所有对象都希望在“被处决前吃上最后一餐”。

C#编译器会将终结器变异成一个名为Finalize的protected override方法,并且(从IL中可以看出)终结器的方法主体的代码被放到一个try块中,finally块则放入了一个对base.Finalize的调用。

.method family hidebysig virtual instance void Finalize() cil managed
{
.maxstack 1
L_0000: nop
L_0001: ldstr "Finalize"
L_0006: call void [mscorlib]System.Console::WriteLine(string)
L_000b: nop 
L_000c: nop 
L_000d: leave.s L_0017
L_000f: ldarg.0 
L_0010: call instance void [mscorlib]System.Object::Finalize()
L_0015: nop 
L_0016: endfinally 
L_0017: nop 
L_0018: ret 
.try L_0000 to L_000f finally handler L_000f to L_0017
}

7. Finalize什么时候会被调用

第0代满时;代码显示调用System.GC的静态方法Collect;Windows报告内存不足;CLR卸载AppDomain;CLR关闭。CLR使用一个特殊的、专用的县城来调用Finalize方法。

8. 终结操作揭秘

应用程序创建一个新对象时,new操作符会从堆中分配内存。如果对象的类型你给定义了Finalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的一个指针放到一个终结列表(finalization list中。当垃圾回收开始时,如果对象被判定为垃圾(并且定义了终结方法),那么在终结列表里的指针会被移除,并追加到freachable队列中,追加到这个队列中的对象不再被视为垃圾,他获得了重生。之后,追加到freachable中的指针会从freachable队列中移除,并调用Finalize方法。垃圾回收器下一次被调用时,这些已终结的对象成为真正的垃圾。(死亡→重生→死亡)

9. Dispose模式:强制对象清理资源

Finalize可以确保资源、内存被释放,但是,它的调用时间是不能保证的。另外,由于它不是公共方法,所以类的用户不能显示调用它。

只要实现IDisposable接口的类型都可以使用using语句。编译器会自动生成一个try/finally块,会在finally中调用IDisposable的Dispose方法

10. 代

代(generation是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。刚添加到托管堆的对象称为0代,垃圾回收器从未检查过他们。当垃圾回收器回收0代对象之后,存活下来的对象会升至第1代对象,只有当0代容量超过预算,并且1代占据了太多的内存的时,垃圾回收器才有可能检查并回收1代(和0代)对象。1代中存活下来的对象会升至第2代。注意CLR的托管堆最多只有2代,每一代中的预算容量根据不同的情况会有所变化。

posted @ 2013-06-07 10:34  中本傻  阅读(209)  评论(0编辑  收藏  举报