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

How does the C# garbage collector find objects whose only reference is an interior pointer?

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

如果发生垃圾回收,可能 对某些对象的引用是通过这些内部指针之一进行的,如:

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需要找到对象的开头并将整个对象视为可达。它是如何做到的?它是否必须扫描整个堆以查找包含该指针的对象?这似乎很慢。

您的代码compiles to

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

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

垃圾收集器的工作分为三个基本步骤:

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

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

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

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

在流程结束时,只有内部引用的对象不会被标记,因为没有从根引用到它们的路径。这也处理了所有循环引用。这些对象被视为 dead 并将在下一步中收集(包括调用终结器,即使不能保证这样做)。

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

确实,GC 需要一些时间来完成所有这些,但由于一些优化,它仍然可以相当快地完成。其中一项优化是将堆分成 。所有新分配的对象都是第 0 代。第一次收集后幸存的所有对象都是第 1 代,依此类推。仅当收集低代未释放足够内存时,才会收集高代。所以,不,GC并不总是要扫描整个堆。

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

垃圾收集器将有一种快速的方法从托管内部指针找到 object 的开始。从那里它可以明显地将 object 标记为 "referenced" 在进行扫描阶段时。

没有 Microsoft 收集器的代码,但他们会使用类似于 Go 的 span table 的东西,它可以快速查找不同的 "spans" 内存,您可以在其中键入最多指针的有效 X 位取决于您选择的跨度有多大。从那里他们利用每个跨度包含 X 个相同大小的对象这一事实来非常快速地找到你拥有的对象的 header。这几乎是一个 O(1) 操作。显然 Microsoft 堆会有所不同,因为它是按顺序分配的,而不考虑 object 大小,但它们将具有某种 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<<PageShift);
if(s->sizeclass == 0) {
    obj = p;
} else {
    uintptr size = s->elemsize;
    int32 i = ((const byte*)obj - p)/size;
    obj = p+i*size;
}

请注意,.NET 垃圾收集器是一个复制收集器,因此只要 object 在垃圾 collection 循环期间移动,就需要更新 managed/interior 指针。 GC 将根据 JIT 时已知的方法参数知道每个堆栈帧在堆栈内部指针中的位置。