前几天学习了CLR垃圾收集原理和基本算法,但是那些是仅仅相对于托管堆而言的,任何非托管资源的类型,例如文件、网络资源等,都必须支持一种称为终止化(finalization)的操作。
终止化
终止化操作允许一种资源在他所占的内存被回收之前首先执行一些清理工作。要提供终止化操作操作,必须为类型实现一个名为Finalize的方法。当垃圾收集器判定一个对象为可收集的垃圾时,它便会调用该对象的Finalize方法(如果存在的话)。
C#为定义Finalize方法提供了特殊的语法看下面代码;
{
private IntPtr handler;
public OSHandler(IntPtr handler)
{
handler = handler;
}
//当垃圾收集器执行时,该析构函数将被调用,它将关闭非托管资源句柄
~OSHandler()
{
CloseHandler(handler);
}
public IntPtr ToHandler()
{
return handler;
}
//释放非托管资源
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static bool CloseHandler(IntPtr handler);
}
查看中间语言
Finalize() cil managed
{
// 代码大小 26 (0x1a)
.maxstack 1
.try
{
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld native int FinalizeStudy.OSHandler::'handler'
IL_0007: call bool FinalizeStudy.OSHandler::CloseHandler(native int)
IL_000c: pop
IL_000d: nop
IL_000e: leave.s IL_0018
} // end .try
finally
{
IL_0010: ldarg.0
IL_0011: call instance void [mscorlib]System.Object::Finalize()
IL_0016: nop
IL_0017: endfinally
} // end handler
IL_0018: nop
IL_0019: ret
} // end of method OSHandler::Finalize
会发现,析构函数被编译器编译为Finalize函数,并且使用了异常处理。
这样当未来某个时刻垃圾收集器判定对象为可收集的垃圾时,它会看到该类型定义有一个Finalize方法,于是它便会调用该方法,从而允许CLoseHandler函数来关闭其中的非托管资源。在Finalize方法返回之后的某个时刻,该OSHandler对象在托管堆中所占的内存才会被回收。
应该避免使用Finalize方法。有以下原因:
- 实现了Finalize的对象其代龄会被提高,增加内存的压力,声音被该对象直接或者间接引用的对象的代龄也将被提升(以后学习代龄)。
- 终止化对象的分配花费的时间较长,因为指向它们的指针必须被放在终止化链表上;
- 强制垃圾收集器执行Finalize方法会极大的损失程序的性能;
- 不能控制Finalize方法何时执行。对象可能会一直占有着资源,直到出现垃圾收集;
- CLR不对Finalize方法的执行顺序做任何的保障。加入对象包含指向另一个对象的指针,两个对象都可能会被垃圾收集,顺序的不一样会导致结果不可预期。靠,个人感觉这就是一个bug。
终止化操作的内部机理
创建一个新对象,new先为对象在托管堆上面分配内存。如果对象的类型定义了FInalize方法,那么在该类型的实例被调用之前,指向该对象的一个指针将被放到一个称为终止化链表(finalization list)的数据结构里面。终止化链表是一个由垃圾收集器控制的内部数据结构。链表上的每一个条目都引用着一个对象。这实际告诉垃圾收集器在回收这些对象的内存之前要首先调用它们的Finalize方法。
当垃圾收集检测到可收集的垃圾时,垃圾收集器会扫描终止化链表是否有执行可收集垃圾的对象,当找到这样的指针,它们会从终止化链表移除,并添加到一个称为终止化可达列表(freachable queue)的数据结构上。在终止化可达列表上出现的对象表示该对象的Finalize方法即将被调用,当垃圾收集完毕后,没有Finalize的对象的内存将被回收,实现了Finalize的对象内存却不能被回收,因为他们的Finalize方法还没有被调用。CLR有一个特殊的高优先级的线程用来专门调用Finalize方法。该线程可以避免线程同步问题。
非常有意义的是,当垃圾收集器将一个对象从终止化链表转移到终止化可达队列时,该对象不再认为是可收集的垃圾对象,它的内存也就不可能被回收。到此为止,垃圾收集器完成了垃圾对象的鉴别工作,一些原先认为是垃圾的对象现在被认为不是垃圾,从某种意义上来说,对象又“复苏”了。当第一次垃圾收集执行完毕后,特殊的CLR线程将会清空终止化可达队列中的对象,同时执行其中某个对象的Finalize方法。
等下一次垃圾收集执行的时候,它会看到这些终止化对象已经成为真正的垃圾对象,这样实现了Finalize的对象的内存才被完全回收。 实际上终止化对象需要执行两次垃圾收集才能释放它所占用的内存。实际上由于代龄的提高,可能收集次数会多于两次。上面这些玩意在Effective C#里面也讲过,以前没有看懂。
Dispose模式
感觉CLR的终止化是个吃力不讨好的玩意
- 分配起来慢(加入终止化链表),
- 收集起来更慢,先是加入可达终止化列表,让对象复活,二次垃圾回收才能收集;
- 不能人为的控制,长时间占用内存;
- 增加对象的代领,更是不可饶恕。
- 怎么办??
微软总是NB的,作者总是掉人胃口的,CLR提供了显示释放或者关闭对象的能力,但是类型需要实现一种被称为Dispose的模式(当然有一些约定)。如果一个类型实现了Dispose模式,使用该类型的开发人员将能够知道当对象不再被使用时如何显式地释放掉它所占用的资源。
新版本的OSHandler实现,应用了Dispose模式:
{
private IntPtr handler;
public OSHandler(IntPtr handler)
{
this.handler = handler;
}
//当垃圾收集器执行时,该析构函数将被调用,它将关闭非托管资源句柄
~OSHandler()
{
Dispose(false);
}
public IntPtr ToHandler()
{
return handler;
}
//释放非托管资源
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static bool CloseHandler(IntPtr handler);
public void Dispose()
{
//因为对象的资源被显示清理,所以在这里阻止垃圾收集器调用Finalize方法
GC.SuppressFinalize(this);
//进行实际清理工作
Dispose(true);
}
//可以替换Dispose方法
public void Close()
{
Dispose();
}
//执行清理工作,protected为了子类
protected void Dispose(bool disposing)
{
//线程安全
lock (this)
{
if (disposing)
{
//对象正在被被显式关闭,此时可以引用其他对象,因为Finalize方法还没有被执行
}
}
if(IsValid)
{
//如果handler有效,那么关闭之
CloseHandler(handler);
handler=InvalidHandler;//置为无效,防止多次调用
}
}
//返回一个无效的句柄值
public IntPtr InvalidHandler{get{ return IntPtr.Zero;}}
//判断句柄是否有效
public bool IsValid { get { return handler != InvalidHandler; } }
}
调用上面的Dispose或者Close方法只是显示释放非托管资源,并不会释放托管堆中占用的内存,释放对象内存的工作仍然由垃圾收集器负责,当然释放时间仍然是不确定的。
上面的代码中Finalize中Dispose方法的disposing参数被设为fasle。这将告诉Dispose方法不应该执行任何其他对象的代码。在Close和无参Dispose方法中disposing参数为true,因为是手动执行,程序逻辑可以控制,可以在if中执行代码。调用SuppressFinalize主要是为了避免终止化对象给垃圾收集器带来负担。
既然已经有了手动关闭的方法,为什么还要实现Finalize方法呢,因为我们不能保证程序的使用者能够一定调用Dispose方法或者Close方法,如果不调用将会造成资源浪费,甚至系统崩溃,但是这不是使用者的错误,我们的程序应该考虑到这一点,实现Finalize就是为了防止这种情况出现,作为一个后备吧。
睡觉~~~~~~~~~~~~