#1 垃圾回收平台的基本工作原理:
访问一个资源所需的具体步骤:
1)调用IL指令newobj,为代表资源的类型分配内存。在C#中使用new操作符,编译器就会自动生成该指令。
2)初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器负责设置该初始状态。
3)访问类型的成员(可根据需要反复)来使用资源。
4)摧毁资源的状态以进行清理。正确清理资源的代码要放在Finalize, Dispose和Close方法。
5)释放内存。垃圾回收器独自负责这一步。
托管堆如何知道应用程序不再用一个对象?
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针(NextObjPtr),用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
newobj指令将导致CLR执行以下步骤:
1) 计算类型(及其所有基类型)的字段所需要的字节数。
2) 加上对象的开销所需要的字节数。包括类型对象指针和同步索引块。32位程序需要增加8字节,64位程序需要增加16字节。
3) CLR检查保留区域是否能够提供分配对象所需要的字节数,如有必要就提交存储。如果托管堆有足够的可用空间,对象会被放入。
托管堆上连续分配的对象会由于引用的locality而获得性能上的提升,而且对象可以全部驻留在CPU缓存中,不会因为cache miss而被迫访问较慢的RAM.
托管堆之所有有这些好处,是因为它做了一个假设--地址空间和存储是无限的。托管堆通过垃圾回收器来允许它做这样的假设。
应用程序调用new操作符创建对象时,如果第0代堆满,执行一次垃圾回收。在一次垃圾回收中存活下来的对象被提升到另一代(例如第1代)。
#2 垃圾回收算法:
每个应用程序都包含一组根(root)。静态字段,方法参数和局部变量均被认为是一个根。只有引用类型的变量才被认为是根。值类型的变量永远都不被认为是根。此外,CPU寄存器也被视作根。
垃圾回收器开始执行时,假设堆中的所有对象都是垃圾,然后通过标记(对有跟引用的进行标记)和压缩(回收没有标记的对象)进行垃圾回收。
标记阶段:垃圾回收器沿着线程栈上行以检查所有根,如果发现一个跟(root)引用了一个对象(直接引用或间接引用),就在对象的"同步索引字段"上开启一位(将一个bit设置为1)进行标记。
压缩阶段:垃圾回收器线性的遍历堆,以寻找未标记对象的连续内存块。若果内存块较小,垃圾回收器会忽略该块。移动内存中的对象后,包含"指向这些对象的指针"的变量和CPU寄存器现在都会变得无效,垃圾回收器需要遍历修改所有根来指向新的内存位置。
#3 垃圾回收与调试:
class DebuggingRoots { public static void Go() { var t = new System.Threading.Timer(TimerCallback, null, 0, 2000); Console.ReadLine(); // 在ReadLine之后引用t,这种方式会被编译器优化掉 //t = null; // 在ReadLine之后引用t,防止其在Dispose方法返回之前被垃圾回收 //t.Dispose(); } private static void TimerCallback(Object o) { Console.WriteLine("In TimerCallback: " + DateTime.Now); // 出于演示目的强制执行垃圾回收 GC.Collect(); } }
#4 使用终结操作释放本地资源:
System.Threading.Mutex类型要打开一个Windows互斥体内核对象(本地资源)并保存其句柄,并在调用Mutex的方法时使用该句柄。
值类型(含所有枚举类型)、集合类型、String、Attribute、Delegate和Exception所代表的资源无需执行特殊的清理操作。如果一个类型代表着(或包装着)一个非托管资源(比如文件、数据库连接、套接字、mutex、位图、图标等),在对象的内存准备回收时,必须执行一些清理代码。实现了Finalize方法的任何类型实际上是在说,它的所有对象都希望"在被处决之前吃上最后一餐"。
1) 使用CriticalFinalizerObject类型确保终结
CLR赋予这个类以下三个功能:
首次构造任何CriticalFinalizerObject派生类型的一个对象时,CLR立即对继承层次结构中的所有Finalize方法进行JIT编译以确保其肯定得到执行。如果不对Finalize方法进行提前编译,则在内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这会阻止方法的执行,造成本地资源泄露。此外,如果Finalize方法中的代码引用了另外一个程序集中的一个类型,而且CLR在寻找这个程序集时失败,也会造成资源得不到释放。
CLR是在调用了非CriticalFinalizerObject派生类型的Finalize方法之后,才调用CriticalFinalizerObject派生类型的Finalize方法。例如,FileStream类的Finalize方法可以放心的将数据从内存缓冲区flush到磁盘,它知道此时磁盘文件还没有关闭。
如果AppDomain被一个宿主应用程序(SQL Server或Asp.NET)强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。
2) SafeHandle类型及其派生类型
对于SafeHandle类,需要注意: 其一,它派生自CriticalFinalizerObject,这确保它会得到CLR的特殊对待; 其二,它是一个抽象类,必须有另外一个类从该类派生,并重写受保护的构造器、抽象方法ReleaseHandle以及抽象属性IsValid的get访问器方法。
3)使用SafeHandle类型与非托管代码进行互操作
与非托管代码进行交互时,SafeHandle提供了另外两个功能: CLR调用Win32 CreateEvent函数,函数返回到托管代码时,新的SafeWaitHandle(CLR知道其从SafeHandle派生)对象的构造以及句柄的赋值是在非托管代码中发生的,不可能被一个ThreadAbortException打断。
SafeHandle派生类的最后一个功能是防止有人利用潜在的安全漏洞。当一个线程可能试图使用一个本地资源,同时另一个线程试图释放该资源,这可能造成一个句柄循环使用漏洞。SafeHandle类防范这个安全隐患的办法是使用引用计数。
CriticalHandle类除了不提供引用计数器功能,其他方面与SafeHandle类相同。CriticalHandle类及其派生类通过牺牲安全性来换取更好的性能。
#5 对托管资源使用终结操作:
虽然终结操作几乎专供释放本地资源,但偶尔也用于托管资源。Finalize方法中调用的任何代码都不能使用其他任何可能已经终结的对象。
即使类型的实例构造器抛出了异常,类型的Finalize方法也会被调用。因此,Finalize方法不应假设对象处于良好、一致的状态。
设计一个类型时,处于性能方面的原因,最好避免使用Finalize方法,因为必须进行额外的处理(分配的时候要将指针放到终结列表,对象可能会提升到较老的代,回收时须进行额外处理).
Finalize方法在垃圾回收发生时运行,而垃圾回收可能在应用程序请求更多内存时才发生;CLR不保证各个Finalize方法的调用顺序;调用静态方法需要注意方法是否在内部访问已终结了的对象。完全可以放心的访问值类型的实例,或者没有定义Finalize方法的引用类型的实例。
#6 导致Finalize方法被调用的5种事件:
第0代满(最常见的方式) 代码显示调用System.GC的静态方法Collect Windows报告内存不足 CLR卸载AppDomain CLR关闭
每个Finalize方法大约有2秒时间返回,所有Finalize方法有40秒钟进行返回。如果超时,CLR会直接杀死进程。
#7 终结操作揭秘:
终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象--该对象定义了Finalize方法,在回收该对象的内存之前,应调用它的Finalize方法。
被判定为垃圾的对象,如果定义了Finalize方法,其终结列表中的指针将被移至freachable队列中。一个特殊的高优先级CLR线程负责调用Finalize方法。如果一个对象在freachable队列中,它就是可达的,不是垃圾。可终结的对象需要执行两次或以上(提升至另一代)垃圾回收才能释放他们占用的内存。
#8 Dispose模式:强制对象清理资源:
public abstract class SafeHandleEx : IDisposable { public void Dispose() { // 传递true,导致可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用 this.Dispose(true); } public void Close() { // 传递true,导致可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用 this.Dispose(true); } public ~SafeHandleEx() { // 传递false,导致不能安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法可能已经调用 this.Dispose(false); } protected virtual void Dispose(Boolean disposing) { if (disposing) { // 在该if语句中,可以安全的访问引用其他对象的字段,因为其他这些对象的Finalize方法还没有调用 } GC.SuppressFinalize(this); } }
#9 使用实现了Dispose模式的类型
public static void Go() { Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; FileStream fs = new FileStream("Temp.dat", FileMode.Create); fs.Write(bytesToWrite, 0, bytesToWrite.Length); // 显示关闭文件 ((IDisposable)fs).Dispose();
// 必须调用Dispose之后调用该方法,否则报告文件正在被占用
File.Delete("Temp.dat");
}
确定必须清理资源时,确定可以安全的调用Dispose或Close,并希望将对象从终结表中删除,禁止对象提升从而提升性能时,才调用Dispose或Close。
#10 使用C# using语句
#11 StreamWriter对FileStream的依赖
#12 手动监视和控制对象的生存期(GCHandle)