提问人:munificent 提问时间:11/22/2017 更新时间:11/28/2017 访问量:2934
C# 垃圾回收器如何查找其唯一引用是内部指针的对象?
How does the C# garbage collector find objects whose only reference is an interior pointer?
问:
在 C# 中,据我所知,params 是通过仅传递相关值的原始地址来传递的。该地址可以是指向数组中的元素或对象中的字段的内部指针。ref
out
如果发生垃圾回收,则对某个对象的唯一引用可能是通过以下内部指针之一,如下所示:
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 需要找到对象的开头,并将整个对象视为可访问。它是如何做到的?它是否必须扫描整个堆以查找包含该指针的对象?这似乎很慢。
答:
您的代码编译为
IL_0001: newobj instance void Foo::.ctor()
IL_0006: ldflda int32 Foo::'field'
IL_000b: call void Foo::Increment(int32&)
AFAIK,只要地址在堆栈上(直到完成),该指令就会创建对包含字段的对象的引用。ldflda
call
评论
垃圾回收器分三个基本步骤工作:
- 标记所有仍处于活动状态的对象。
- 收集未标记为活动的对象。
- 压缩内存。
您关心的是第 1 步:GC 如何确定它不应该收集后面的对象和参数?ref
out
当 GC 执行集合时,它以一个状态开始,在该状态中,没有一个对象被视为活动对象。然后,它从根引用开始,并将所有这些对象标记为活动对象。根引用是堆栈和静态字段中的所有引用。然后,GC 递归进入标记的对象,并将从它们引用的所有对象标记为活动对象。重复此操作,直到未找到尚未标记为活动的对象。此操作的结果是一个对象图。
or 参数在堆栈上具有引用,因此 GC 会将相应的对象标记为活动对象,因为堆栈是对象图的根。ref
out
在该过程结束时,不会标记仅具有内部参照的对象,因为根参照中没有路径可以到达它们。这也照顾了所有循环引用。这些对象被视为无效对象,并将在下一步中收集(包括调用终结器,即使无法保证这一点)。
最后,GC 会将所有活动对象移动到堆开头的连续内存区域。内存的其余部分将填充为零。这简化了创建新对象的过程,因为它们的内存始终可以在堆的末尾分配,并且所有字段都已具有默认值。
的确,GC 需要一些时间来完成所有这些工作,但由于一些优化,它仍然相当快地完成。其中一项优化是将堆分成几代。所有新分配的对象都是第 0 代。在第一个集合中幸存下来的所有对象都是第 1 代,依此类推。只有当收集低世代不能释放足够的内存时,才会收集较高的世代。所以,不,GC 并不总是必须扫描整个堆。
您必须考虑,虽然收集需要一些时间,但分配新对象(这比垃圾回收更频繁地发生)比其他实现要快得多,在其他实现中,堆看起来更像是瑞士奶酪,您需要一些时间来为新对象找到一个足够大的洞(您仍然需要初始化)。
评论
ref
ref
ref
ref
垃圾回收器将有一种快速的方法从托管的内部指针中查找对象的开头。从那里,它可以在进行扫描阶段时明显地将对象标记为“参考”。
没有 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 时已知的方法参数来了解每个堆栈内部指针在堆栈内部指针中的位置。
评论
new Foo()
new Foo()
在这里的堆栈上。这与 相同。var f = new Foo(); Increment(ref f.field);
f