前往精雕的车上,我读着 CLR via C# 的托管堆和垃圾回收一章。读着读着突然想到,上次的怀疑没那么麻烦,因为 b
此时仍然处于栈上,是属于可访问的,所以不应该被回收。书中列举了一个奇怪的代码:
using System.Threading;
static void Main()
{
Timer t = new Timer(TimerCallback, null, 0, 2000);
Console.ReadLine();
}
static void TimerCallback(Object o)
{
// 只会输出一次,之后就不再输出
Console.WriteLine("In TimerCallback: " + DateTime.Now);
GC.Collect();
}
书上的解释:
然后,垃圾回收器检查应用程序的根,发现在初始化之后,
Main()
方法再也没有用过变量t
。既然应用程序没有任何变量引用Timer
对象,垃圾回收自然会回收分配给它的内存;这使计时器停止触发,并解释了为什么TimerCallback()
方法只调用了一次。
不过这是罕见中的罕见情况,因为 System.Threading.Timer
使用的是线程池分配的新的触发线程,是异步的(System.Windows.Forms.Timer
用的是 SetTimer()
API,在消息循环中处理,是同步的),所以在新线程中的扫描结果自然是“自己没引用,创建的线程之后也没引用”了。执行预测,那还得是 JIT 编译器的功劳,要不也像我一样只是简单认为它还在栈上。但是,同步的时候就没有这个问题。——这说明即使是完善的 CLR,也很难解决设计上就无法克服的困难。
一些觉得需要认真考虑的是:
- 非确定性析构(
Finalize()
); - f-reachable 队列,以及该队列处于独立的线程上,有元素(某个对象的
Finalize()
时调用); - 终结时的复活(revive)机制,有助于理解
Finalize()
中代码对其他对象的引用的处理; - GC 句柄表;
System.Runtime.InteropServices.GCHandleType.Pinned
的特殊处理方式;- 写屏障(write barrier)的 card table(注意,要分配在连续空间上才有意义)。
由于不用做多个 AppDomain
,所以我要做的模型的回收时机会简单一些。
下面要想的是还原 MonoVTable
和 MonoMethod
的结构,因为这关系到下一个测试:终结器(finalizer)测试的设计。