今天一天

前往精雕的车上,我读着 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,所以我要做的模型的回收时机会简单一些。

下面要想的是还原 MonoVTableMonoMethod 的结构,因为这关系到下一个测试:终结器(finalizer)测试的设计。

分享到 评论