在C#中,据我所知,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需要找到对象的开头并将整个对象视为可达。它是如何做到的?是否必须扫描整个堆以查找包含该指针的对象?这似乎很慢。
答案 0 :(得分:3)
垃圾收集器有三个基本步骤:
您关注的是第1步:GC如何确定它不应该收集ref
和out
参数后面的对象?
当GC执行集合时,它从一个没有任何对象被认为是活动的状态开始。然后它从根引用开始,并将所有这些对象标记为活动。根引用是堆栈和静态字段中的所有引用。然后GC递归地进入标记的对象,并将所有对象标记为从它们引用的活动对象。重复此操作,直到找不到尚未标记为活动的对象。此操作的结果是对象图。
ref
或out
参数在堆栈上有引用,因此GC会将相应的对象标记为活动,因为堆栈是对象图的根。
在进程结束时,没有标记仅具有内部引用的对象,因为根引用中没有到达它们的路径。这也照顾所有循环引用。这些对象被认为是 dead ,将在下一步中收集(包括调用终结器,即使不能保证)。
最后,GC会将所有活动对象移动到堆开头的连续内存区域。内存的其余部分将填充零。这简化了创建新对象的过程,因为它们的内存总是可以在堆的末尾分配,并且所有字段都已经具有默认值。
GC确实需要一些时间来完成所有这些工作,但由于一些优化,它仍能以相当快的速度完成。其中一个优化是将堆分成代。所有新分配的对象都是第0代。第一个集合中存活的所有对象都是第1代,依此类推。只有收集较低代的人才能释放足够的记忆,才会收集更高代。所以,不,GC并不总是必须扫描整个堆。
你必须考虑到,虽然集合需要一些时间,但是分配新对象(比垃圾收集更频繁地发生)比在其他实现中快得多,其中堆看起来更像瑞士奶酪而你需要一段时间找到一个足够大的新洞(你还需要初始化)。
答案 1 :(得分:2)
您的代码compiles to
IL_0001: newobj instance void Foo::.ctor()
IL_0006: ldflda int32 Foo::'field'
IL_000b: call void Foo::Increment(int32&)
AFAIK,ldflda
指令创建对包含该字段的对象的引用,只要该地址在堆栈上(直到call
完成)。