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

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

提问人:munificent 提问时间:11/22/2017 更新时间:11/28/2017 访问量:2934

问:

在 C# 中,据我所知,params 是通过仅传递相关值的原始地址来传递的。该地址可以是指向数组中的元素或对象中的字段的内部指针。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 需要找到对象的开头,并将整个对象视为可访问。它是如何做到的?它是否必须扫描整个堆以查找包含该指针的对象?这似乎很慢。

C# .NET 垃圾回收 CLR 按引用传递

评论

0赞 johnny 5 11/22/2017
我可能错了,但垃圾收集器不会对任何已创建对象的所有引用进行哈希处理,以便快速查找
0赞 SLaks 11/22/2017
@johnny5:那又怎样?它仍然无法判断 是否被任何内容引用。new Foo()
0赞 Blorgbeard 11/22/2017
new Foo()在这里的堆栈上。这与 相同。var f = new Foo(); Increment(ref f.field);
1赞 SLaks 11/22/2017
@Blorgbeard:没有;在那里,存储在一个局部变量中,该变量创建一个 GC 根。在这里,它不是。f
1赞 munificent 11/22/2017
查找 Main() 无济于事。Main 的堆栈没有引用 Foo 的实例。它从不存储在局部变量中,并且在调用 Increment() 之前,临时变量会从堆栈中弹出。据我所知,堆栈上唯一的东西是 Foo 中字段的地址。

答:

3赞 SLaks 11/22/2017 #1

您的代码编译为

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

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

评论

1赞 munificent 11/22/2017
我的理解是 ldflda 推送字段本身的地址(即内部指针),而不是包含字段的对象。因此,ldflda 会弹出对对象的引用并推回字段的地址,这意味着对象本身不再在堆栈上。
0赞 SLaks 11/22/2017
@munificent:我认为它将对象本身作为 GC 根(而不是在堆栈上)推送。不过,我不确定。
0赞 tumtumtum 11/29/2017
LDFLDA 将对象本身从堆栈中弹出。它无法将对象作为 GC 根“推送”,因为它什么时候知道要弹出它?唯一明智的方法是使用堆栈.....我很确定该对象通过内部指针保持活动状态。
1赞 Sefe 11/22/2017 #2

垃圾回收器分三个基本步骤工作:

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

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

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

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

在该过程结束时,不会标记仅具有内部参照的对象,因为根参照中没有路径可以到达它们。这也照顾了所有循环引用。这些对象被视为无效对象,并将在下一步中收集(包括调用终结器,即使无法保证这一点)。

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

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

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

评论

4赞 munificent 11/22/2017
是的,我了解 GC 的一般工作方式。您注意到:“ref 或 out 参数在堆栈上具有引用,因此 GC 会将相应的对象标记为活动对象,因为堆栈是对象图的根。使用 ref/out 参数时,堆栈只有对象(内部指针)内字段的地址,而不是对象本身。给定该内部指针,它如何找到对象的开头?
0赞 Sefe 11/22/2017
@munificent:为什么你认为没有对对象的引用?这是一个字段,它位于一个对象上。因此,也始终存储了一个对象引用。
2赞 Glenn Slayden 4/22/2019
"...也总是存储一个对象引用......“——不,这就是@munificent问题的重点。使用本身会创建运行时方案,GC 无法预见、预测或主动跟踪包含对象句柄。在运行时传播(或“存储”)一个相关的对象句柄,以及每次使用(并且可能没有一个,例如,堆栈上的值类型)可能会破坏首先使用的目的。相反,GC只在GC时间重建这些。查看 stackoverflow.com/a/52829684/147511refrefrefref
0赞 Qwertie 4/9/2020
“只有内部参照的对象没有被标记......这些物品被认为是死的,将被收集“呃......问题是关于“内部指针”(托管指针),如果这就是你的意思,你就错了。指向对象的内部指针不是弱引用。它们将阻止整个对象的 GC。
6赞 tumtumtum 11/28/2017 #3

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

没有 Microsoft 收集器的代码,但他们会使用类似于 Go 的 span 表的东西,它可以快速查找不同的内存“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<<PageShift);
if(s->sizeclass == 0) {
    obj = p;
} else {
    uintptr size = s->elemsize;
    int32 i = ((const byte*)obj - p)/size;
    obj = p+i*size;
}

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

评论

2赞 munificent 11/28/2017
啊,是的。这是有道理的。四处挖掘,我猜 dotnetcore 中的相应代码是 github.com/dotnet/coreclr/blob/master/src/gc/gc.cpp 中第 17143 行的 gc_heap::find_object()。谢谢!