当垃圾收集器在堆中移动数据时,引用是否会被更新?

21

我读到GC(垃圾回收器)为了性能原因移动堆中的数据,但我不太明白为什么,因为它是随机访问内存,也许是为了更好地顺序访问,但我想知道当堆中发生这种移动时,栈中的引用是否会更新。但也许偏移地址保持不变,而垃圾收集器会移动数据的其他部分,但我不确定。

我认为这个问题涉及实现细节,因为并非所有垃圾收集器都可能执行此类优化,或者它们可能执行该优化但不更新引用(如果这是垃圾收集器实现的通用做法)。但我想得到一些特定于CLR(公共语言运行时)垃圾收集器的整体答案。

我也在阅读Eric Lippert的“References are not addresses”文章here,以下段落有点困惑我:

如果你认为一个引用实际上是一个不透明的GC句柄,那么就变得清楚了, 要找到与句柄相关联的地址,您必须以某种方式“固定”对象。 您必须告诉GC“在进一步通知之前,具有此句柄的对象不得在内存中移动, 因为某人可能拥有它的内部指针”。(有多种方法可以做到这一点,这超出了本文的范围。)

这似乎意味着对于引用类型,我们不希望数据被移动。那么我们可以为了性能优化在堆中移动什么?也许我们在那里存储类型信息?顺便说一下,如果你想知道那篇文章是关于什么的,那么Eric Lippert会稍微比较引用和指针,并尝试解释为什么说引用只是地址可能是错误的,即使这是C#的实现方式。

如果我上面的任何假设是错误的,请纠正我。


1
@AdamHouldsworth:但我的问题是想了解它是如何发生的:它是通过在整个对象移动到其他内存地址时更新引用值来维护它们,还是仅通过不移动对象数组的初始地址来使其不需要更改引用值。 - Tarik
@Tarik更新了链接。如果不够用,我可以再找找。我记得在《CLR Via C#》这本书中有详细的解释(顺便说一句,这是一本很棒的书)。 - kha
1
@Tarik确实,这就是为什么我还没有发布答案的原因。我也在等待答案。 - Adam Houldsworth
1
另一个链接提供了更详细的信息(我认为你不可能比这更详细了):http://www.informit.com/articles/article.aspx?p=1409801&seqNum=2 这是你可能会感兴趣的部分:“当垃圾回收发生时,对象B和D占用的内存被回收,这导致托管堆上出现间隙。为了消除这些间隙,垃圾收集器会压缩剩余的活动对象(Obj A、C和E),并将两个空闲块(用于保存Obj B和D)合并成一个空闲块。最后,由于压缩和合并的结果,当前分配指针也会更新。” - kha
你误解了引用段落的意思。它迂回地表达的是,在引用被用于操作指向的对象时,必须有一些机制来确保引用内部的地址不会改变。有多种方法可以提供这种保证。 - Hot Licks
显示剩余3条评论
2个回答

18

是的,在垃圾回收期间会更新引用。这是必要的,因为在堆被压缩时对象会被移动。压缩有两个主要目的:

  • 通过更有效地使用处理器的数据缓存,使程序更加高效。这对于现代处理器来说非常重要,RAM与执行引擎相比极其缓慢,相差两个数量级。当处理器必须等待RAM提供变量值时,它可能会停顿数百个指令。
  • 它解决了堆所遭受的碎片化问题。碎片化是指当释放一个被活动对象所包围的小对象时发生的情况。它是一个无法用于任何其他大于或等于该对象大小的对象的空洞。这对于内存使用效率和处理器效率都不利。请注意,.NET中的LOH(大对象堆)不会被压缩,因此会遭受这种碎片化问题。关于此问题在SO上有很多问题。

尽管Eric的讲解很简单,但对象引用确实只是一个地址。一个指针,完全与您在C或C++程序中使用的类型相同。非常有效,这是必要的。在移动对象之后,GC所要做的就是更新存储在指针中的地址以指向已移动的对象。CLR还允许为对象分配句柄,即额外的引用。在.NET中公开为GCHandle类型,但只有当GC需要帮助确定对象是否应该保持活动状态或不应该被移动时才是必要的。只有在与非托管代码交互时才相关。

不那么简单的是找回那个指针。 CLR致力于确保可以可靠且高效地完成此操作。这些指针可以存储在许多不同的位置。更容易找回的是存储在对象字段、静态变量或GCHandle中的对象引用。较难的是存储在处理器堆栈或CPU寄存器上的指针。例如方法参数和局部变量。

为了实现这一点,CLR需要提供一个保证,使得GC能够始终可靠地遍历线程的堆栈。这样它就可以找回存储在堆栈帧中的局部变量。然后它需要知道在这样的堆栈帧中 哪里查找,这是JIT编译器的工作。当编译一个方法时,它不仅会生成该方法的机器代码,还会构建一个描述指针存储位置的表格。您可以在此帖子中了解更多详细信息。


1
想象一个世界,其中引用是通过句柄间接寻址的地址,这些句柄是索引到地址表中的。当然,每次访问都会稍微变慢,但我们支付完全相同的虚函数调用惩罚而不必过度强调。实现这样的方案时,您很快就会意识到,当对象被释放时,您会在地址表中获得一个空洞,现在您回到了与之前相同的问题:如何摆脱这些空洞以保持表小。这里没有免费的午餐!我喜欢将此作为面试问题的变体提出。 - Eric Lippert
1
@EricLippert,68K型号(Lisa/MacIntosh)的原始苹果操作系统就是这样工作的,即通过双重间接寻址来访问内存。 - adrianm
@adrianm:即使在C++中,也不难想象出一些情况下使用方案会更有优势。我一直在尝试使用这个原则编写一个嵌入式ARM字符串库,针对的是内存小于128K(在很多情况下小于16K)的机器。每个C++对象都持有一个16位句柄[有了128K的RAM,可以肯定字符串的数量足够小],然后引用一个16位字符串池缩放偏移量,并要求只有一个C++对象标识任何句柄(必须构造/析构),但允许多个句柄标识一个字符串,应该... - supercat
...工作得相当不错。我设想每个引用的成本为4个字节(池内和池外各2个字节);长达64字节的字符串实例将花费一个字节加上字符串内容。更长的字符串将涉及时间/空间折衷(我可能会有一个“平面数组”类型,它将表现为一个字符串,但存储为要连接的字符串数组,除了第一个和最后一个之外的所有字符串都被分组成32-63个字符的块)。因此,串联和子字符串提取通常需要为新字符串的开头/结尾创建新的短字符串实例,或者... - supercat
连接两个字符串时,可以重用现有字符串的大部分内容。如果每个句柄的成本为四个字节,尝试使用引用计数来支持共享是没有意义的。 - supercat

6

C++\CLI In Action,有一节关于内部指针和固定指针的内容:

C++/CLI提供了两种解决此问题的指针。第一种称为内部指针,每次对象被重新定位时,运行时会更新其位置以反映所指向的对象的新位置。内部指针指向的物理地址永远不会保持不变,但它始终指向相同的对象。另一种称为固定指针,它防止GC重新定位对象;换句话说,它将对象固定在CLR堆中的特定物理位置上。在某些限制下,可以在内部、固定和本机指针之间进行转换。

由此可知,引用类型确实会在堆中移动,它们的地址也会发生变化。在标记和清扫阶段之后,对象会在堆内进行压缩,实际上是移动到新地址。CLR负责跟踪实际存储位置并使用内部表更新这些内部指针,确保在访问时仍然指向对象的有效位置。

这里有一个例子:

ref struct CData
{
    int age;
};

int main()
{
    for(int i=0; i<100000; i++) // ((1))
        gcnew CData();

    CData^ d = gcnew CData();
    d->age = 100;

    interior_ptr<int> pint = &d->age; // ((2))

    printf("%p %d\r\n",pint,*pint);

    for(int i=0; i<100000; i++) // ((3))
        gcnew CData();

    printf("%p %d\r\n",pint,*pint); // ((4))
    return 0;
}

这是解释:

在示例代码中,你创建了100,000个孤立的CData对象((1)),以便可以填满CLR堆的大部分空间。然后,你创建了一个存储在变量中的CData对象,并且((2))创建了指向该CData对象中int成员age的内部指针。接着,你打印出指针地址以及指向的int值。现在,((3))你创建另外100,000个孤立的CData对象;在这条线路上,垃圾回收循环发生了(早期创建的孤立对象((1))因为没有被引用而被收集)。请注意,你没有使用GC::Collect调用,因为那不保证强制进行垃圾回收循环。正如在前一章节的垃圾回收算法讨论中已经看到的那样,((4))GC通过删除孤立对象来释放空间,以便可以进行进一步的分配。在代码结束时(此时已发生垃圾回收),你再次打印出指针地址和age的值。这是我在我的机器上得到的输出(请注意,地址将因机器而异,因此你的输出值将不同):
012CB4C8 100
012A13D0 100

这是我们如何拥有一个内部类的方式吗?:var person = new Person(); var name = person.Name;,那么在C#中,name是否成为了内部指针类型的引用? - Tarik
在C#中,“内部指针”的概念并不存在,只存在于不安全代码内部。你可以将其逻辑上看作是一个“内部指针”,它将始终引用分配期间正确的对象地址。在进行不安全代码时,你会检索到一个内部指针,而不是本机指针。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接