C#垃圾收集器如何找到唯一引用为内部指针的对象?

在C#中,据我所知, refout params只传递相关值的原始地址。 该地址可以是指向数组中的元素或对象内的字段的内部指针。

如果发生垃圾收集,则对某个对象的唯一引用可能是通过其中一个内部指针,如:

 using System; public class Foo { public int field; public static void Increment(ref int x) { System.GC.Collect(); x = x + 1; Console.WriteLine(x); } public static void Main() { Increment(ref new Foo().field); } } 

在这种情况下,GC需要找到对象的开头并将整个对象视为可达。 它是如何做到的? 是否必须扫描整个堆以查找包含该指针的对象? 这似乎很慢。

垃圾收集器将有一种快速方法从托管内部指针查找对象的开始。 从那里开始,在进行扫描阶段时,它显然可以将对象标记为“参考”。

没有Microsoft收集器的代码,但是他们会使用与Go的span表类似的东西,它快速查找内存的不同“跨度”,你可以根据你的大小来键入指针的最重要的X位选择跨度。 从那里他们使用这样的事实,即每个跨度包含相同大小的X个对象,以便快速找到您拥有的对象的标题。 这几乎是O(1)操作。 显然,Microsoft堆将是不同的,因为它是按顺序分配的,不考虑对象大小,但它们将具有某种O(1)查找结构。

https://github.com/puppeh/gcc-6502/blob/master/libgo/runtime/mgc0.c

 // Otherwise consult span table to find beginning. // (Manually inlined copy of MHeap_LookupMaybe.) k = (uintptr)obj>>PageShift; x = k; x -= (uintptr)runtime_mheap.arena_start>>PageShift; s = runtime_mheap.spans[x]; if(s == nil || k < s->start || (const byte*)obj >= s->limit || s->state != MSpanInUse) return false; p = (byte*)((uintptr)s->start<sizeclass == 0) { obj = p; } else { uintptr size = s->elemsize; int32 i = ((const byte*)obj - p)/size; obj = p+i*size; } 

请注意,.NET垃圾收集器是一个复制收集器,因此每当在垃圾收集周期中移动对象时,都需要更新托管/内部指针。 GC将根据JIT时间已知的方法参数知道堆栈内部指针在每个堆栈帧的位置。

您的代码编译为

  IL_0001: newobj instance void Foo::.ctor() IL_0006: ldflda int32 Foo::'field' IL_000b: call void Foo::Increment(int32&) 

AFAIK, ldflda指令创建对包含该字段的对象的引用,只要该地址在堆栈上(直到call完成)。

垃圾收集器有三个基本步骤:

  1. 标记所有仍然存活的对象。
  2. 收集未标记为活动的对象。
  3. 压缩内存。

您关心的是第1步:GC如何确定它不应该收集ref和paras后面的对象?

当GC执行集合时,它从一个没有任何对象被认为是活动的状态开始。 然后它从根引用开始,并将所有这些对象标记为活动。 根引用是堆栈和静态字段中的所有引用。 然后GC递归地进入标记的对象,并将所有对象标记为从它们引用的活动对象。 重复此过程,直到找不到尚未标记为活动的对象。 该操作的结果是对象图

refout参数在堆栈上有引用,因此GC会将相应的对象标记为活动,因为堆栈是对象图的根。

在进程结束时,没有标记仅具有内部引用的对象,因为根引用中没有到达它们的路径。 这也照顾了所有循环引用。 这些对象被认为是死的 ,将在下一步中收集(包括调用终结器,即使不能保证)。

最后,GC会将所有活动对象移动到堆开头的连续内存区域。 内存的其余部分将填充零。 这简化了创建新对象的过程,因为它们的内存始终可以在堆的末尾分配,并且所有字段都已具有默认值。

确实,GC需要一些时间来完成所有这些工作,但由于一些优化,它仍然可以相当快地完成。 其中一个优化是将堆分成几代 。 所有新分配的对象都是第0代。第一个集合中存活的所有对象都是第1代,依此类推。 只有收集较低代的人才能释放足够的记忆,才会收集更高代。 所以,不,GC并不总是必须扫描整个堆。

你必须考虑到,虽然集合需要一些时间,但是分配新对象(比垃圾收集更频繁地发生)比在其他实现中快得多,其中堆看起来更像瑞士奶酪,你需要一些时间来为新对象找到一个足够大的洞(你仍然需要初始化)。