关于GC
小结:
1. 每个应用程序都有一组根(Root),一个根是一个存储位置,其中包含着一个指向引用类型对象的内存指针。该指针或者指向一个托管堆中的对象,或者被设为null。
如:静态字段被认为是根,方法参数或局部变量也被认为是一个根,对于变量,仅当变量是引用类型时才被认为是根,值类型的变量永远不被认为是根。
2. 垃圾收集
当垃圾收集器开始工作时,它会首先假设托管堆中的所有对象都是可收集的垃圾。也就是说,垃圾收集器假设线程堆栈中没有一个变量引用堆中的对象,没有CPU寄存器引用堆中的对象,而且也没有静态字段引用堆中的对象。
A. 垃圾收集的第一步(标记阶段)
垃圾收集器遍历线程堆栈,检查所有的根,如果发现根引用了一个对象,那么就在该对象的同步块索引字段上设置一个bit位以表示该对象还在被引用,对象就是这样被标记的。接着,垃圾收集器检查下一个根,一旦检查完所有根,托管堆上就会出现一组已标记和未标记的对象,已标记的对象是应用程序的可达对象,未标记的对象是不可达对象,被当作是垃圾。如图:
B. 垃圾收集的第二步(压缩阶段)
该阶段垃圾收集器线性的遍历堆以寻找包含未标记对象的连续区块。
如果垃圾收集器找到了容量较小的内存块,那么垃圾收集器将忽略这些内存块不计。如果垃圾收集器找到了较大的连续内存块,那么垃圾收集器就会把内存中的一些非垃圾对象搬移到这些连续内存块中以压缩托管堆。
显然,搬移内存中的对象将使所有包含这些对象指针的变量和CPU寄存器变得无效。因此,垃圾收集器必须重新访问应用程序的所有根,并修改它们使其指向这些对象的新的内存位置。另外,如果任何对象中包含有指向另一移动过的对象字段,那么垃圾收集器同样负责矫正这些字段,压缩后的图如:
3. 使用Finalization来释放本地资源
在垃圾回收时,对于大多数类型只要回收其内存空间即可,但有些类型除了使用内存以外,还需要使用本地资源。如,System.IO.FileStream类型,该类型需要打开一个文件(本地资源),并保存句柄,然后该类型的Read和Write方法才能使用该句柄来操作文件。类似地,System.Threading.Mutex类型也需要打开一个Windows互斥内核对象(本地资源),并保存其句柄以供随后Mutex的方法被调用时使用。
Finalization是CLR提供的一种机制,它允许对象在垃圾收集器回收其内存之前执行一些资源清理工作。任何包装了本地资源的类型(如:文件、网络连接、套接字、互斥体以及其他类型等),都必须支持Finalization操作。本质上,对象实现了一种命名为Finalize方法(析构函数)。当垃圾收集器判定一个对象为可收集的垃圾时,它便会调用该对象的Finalize方法【如果存在的话】来释放其引用的本地资源。
C#中,Finalize方法的定义如下:
internal sealed class SomeType { ~SomeType();//就是一个析构函数 //... }
在IL Code中可以看见一个
protected override Finalize() { ... }
且这个Finalize方法的IL Code中可以看见该方法中的代码是放在try语句块中的,且在finally语句块中调用了base.Finalize()
一个Finalize(析构)方法内部实现是调用win32 CloseHandle函数来关闭资源的句柄,如果一个包含本地资源的类型没有定义Finalize方法,那么本地资源将不会被关闭(释放),这样将会导致资源泄漏直到进程终止,此时,操作系统将会回收这些本地资源。
4. Finalization操作内部细节
表面上看上去Finalize方法很简单,但是内部实现是很复杂的。如果一个对象定义了Finalize方法(析构函数),那么在该对象的实例构造函数被调用之前,一个指向该对象的指针会被放在Finalization列表里,该Finalization列表由垃圾回收器控制,它是一个内部数据结构,列表里的每个入口都指向相应的对象,Finalize方法在该对象的内存回收之前被调用。如图:
----因为B, G, H没有根(Root)指向它们,且它们又没有定义析构函数,所以它们直接被当作垃圾回收;
----C, E, F, I, J 中定义了析构函数,所以有指向它们的指针放在Finalization List中;
----因为对象C, F 是有根指向它的,所以不是垃圾,而E, I, J 没有根指向它们,但Finalization List里有指针指向它们,即E, I, J中定义了析构函数,所以垃圾收集器在回收它们之前,会将Finalization List中指向它们的指针转移到Freachable queue中,该队列中的指针表明所对应的对象已经做好执行Finalize方法的准备了,即释放所占有的资源,如图:
----B, G, H, E, I, J 被认为是垃圾
----扫描Finalization List 时,发现B, G, H 没有指针指向它们,所以B, G, H 直接被回收
----因为E, I, J 中有定义析构函数,所以Finalization List中指向它们的指针将会移到Freachable queue中。如果一个对象被放在了Freachable queue中,那么该对象还是可以到达的,它还不是垃圾,当它的Finalize方法被执行后,此时它就成了垃圾了,可以被回收了。
一个特别的高优先级的线程专门用于调用Finalize方法,调用完各对象的Finalize方法后的图如:
5. 对象的代
代是垃圾收集器的一种机制,存在的惟一目的是提高应用程序的性能;
垃圾收集器会做如下假设:
A. 对象越新,其生命周期越短
B. 对象越老,其生命周期越长
C. 对部分堆进行垃圾收集比对整个堆进行收集快
1)第0代
当CLR初始化时,托管堆里是没有对象的,此时,新建的对象放进托管堆里叫作第0代,垃圾收集器还没有对其检查过,如图:
----CLR初始化时,会先对第0代分配一定的空间,假设为256KB,因此,当新的对象放进来导致空间不够时,这时垃圾收集器就会启动工作
2)假设对象 A 至 E 已经占满了第0代(256KB),当对象 F 进来时,垃圾收集器开始检查A 至 E 中哪些是垃圾(不可到达的),假设检查出C 和 E 是垃圾,
----A, B, D 从第0代中存活了下来,被移入了第1代,在产生第1代时,CLR也会为第一代分配一定的空间,这里假定是2MB, 第0代大小保持不变,垃圾回收后,第0代中是空的,没有对象,新创建的对象总是放在第0代。第1代中的对象已经被垃圾收集器检查过了。
3)应用程序又在第0代中新建了F 至 K 对象,然后发现B, H, J 不可到达,得收回它们的空间在某个时刻
----现在假设分配对象L, 此时会导致第0代饱和,没有可分配空间,垃圾收集器又启动,但现在的问题是它先检查哪一代(有第0代和第1代)?当垃圾收集器启动后,它会先去检查第1代空间的使用情况【因为第0代的使用情况是已知的,只要垃圾收集器启动就说明第0代已经饱和】,发现第1代中使用的总空间远少于2MB,所以它不会去检查第1代中的对象,而是去检查第0代中的对象,此时,第1代中的垃圾仍然会留在第1代中(B 还存在):
假设分配对象L 至 O, G、L、M 为不可到达
假设此时第1代中的空间使用远少于2MB,垃圾收集器只检查第0代中的对象,L, M被回收
此时,会发现第1代保持缓慢增长,假设现在第1代已经饱和(达到了2MB),且假设分配新对象P 至 S,且设一段时间后A, K, P, R为不可达对象:
当分配T对象,发现第0代已满,第1代也已经饱和,所以此时要对第0代和第1代都进行垃圾收集,然后第0代中存活下来的提升到第1代,第1代中存活下来的提升到第2代,第0代再次变空:
----托管堆中只支持3个代:第0代,第1代,第2代,没有第3代。当CLR初始化时,会预先为这3个代大小设置一个预算值。