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

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

我认为这个问题与实现细节有关,因为并不是所有的垃圾收集器都可以执行这种优化,或者他们可以这样做,但不会更新引用(如果这是垃圾收集器实现中的常见做法)。 不过,我想获得CLR(通用语言运行库)垃圾收集器特有的全面答案。

我也在阅读Eric Lippert的“参考文献不是地址”这篇文章,下面这段文字让我有些困惑:

如果您认为引用实际上是一个不透明的GC句柄,那么很明显,要找到与句柄关联的地址,您必须以某种方式“修复”该对象。 您必须告诉GC“,直到进一步通知为止,带有该句柄的对象不能移入内存中,因为有人可能有内部指针”。 (有很多方法可以做到这一点,这超出了这个熨平板的范围。)

这听起来像是参考类型,我们不希望数据被移动。 那么我们还有什么存储在堆中,我们可以在性能优化中移动? 也许我们在那里存储类型信息? 顺便说一句,如果你想知道那篇文章是关于什么的话,那么Eric Lippert正在比较指针的引用,并尝试解释说引用只是地址,尽管它是C#实现它的方式可能是错误的。

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


是的,引用在垃圾回收期间得到更新。 必须如此,当堆被压缩时,对象会移动。 压实有两个主要目的:

  • 它通过更高效地使用处理器的数据缓存使得程序更高效。 对于现代处理器来说,这是一个非常非常重要的交易,与执行引擎相比,RAM非常缓慢,这是一个重要的两个数量级。 当必须等待RAM提供一个可变值时,处理器可以停止数百条指令。
  • 它解决了堆积如山的碎片问题。 释放由活动对象包围的小对象时会发生碎片。 一个洞不能用于其他任何物品,而只能是等量或较小尺寸的物品。 内存使用效率和处理器效率不佳。 请注意,.NET中的大对象堆LOH没有得到压缩,因此遭受了碎片问题。 关于SO的许多问题。
  • 尽管Eric的教诲,对象引用实际上只是一个地址。 一个指针,与你在C或C ++程序中使用的完全一样。 非常有效,必然如此。 而移动一个对象之后,所有的GC都必须更新存储在该指针中的地址,以移动对象。 CLR还允许为对象分配句柄,额外引用。 在.NET中作为GCHandle类型公开,但只有在GC需要帮助时才需要确定对象应该保持活动状态还是不应移动。 只有在与非托管代码进行交互时才有意义。

    不那么简单的是找回指针。 CLR投入巨资确保可靠,高效地完成。 这样的指针可以存储在许多不同的地方。 容易找到的是存储在对象字段中的对象引用,静态变量或GCHandle。 硬指针存储在处理器堆栈或CPU寄存器中。 例如,适用于方法参数和局部变量。

    CLR需要提供的一个保证是GC可以始终可靠地走过一堆线程。 所以它可以找到存储在堆栈帧中的局部变量。 然后它需要知道在这种堆栈框架中查找的位置,这是JIT编译器的工作。 当它编译一个方法时,它不会为该方法生成机器码,它还会建立一个描述这些指针存储位置的表。 你会在这篇文章中找到更多关于这方面的细节。


    看看C ++ CLI在Action中,有一段关于内部指针vs钉住指针的部分:

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

    由此可以得出结论,参考类型确实在堆中移动,并且他们的地址确实会改变。 在Mark和Sweep阶段之后,对象在堆内压缩,实际上移动到新地址。 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 %drn",pint,*pint);
    
        for(int i=0; i<100000; i++) // ((3))
            gcnew CData();
    
        printf("%p %drn",pint,*pint); // ((4))
        return 0;
    }
    

    解释如下:

    在示例代码中,创建100,000个孤立的CData对象((1)),以便填充CLR堆的很大一部分。 然后创建一个存储在变量中的CData对象和((2))一个指向此CData对象的int成员时间的内部指针。 然后输出指针地址以及指向的int值。 现在, ((3))您创建了另外100,000个孤儿CData对象; 在该行的某处,发生垃圾收集循环(之前创建的孤儿对象((1))被收集,因为它们没有被引用到任何地方)。 请注意,您不使用GC :: Collect调用,因为不能保证强制进行垃圾收集循环。 正如您在上一章讨论垃圾收集算法时已经看到的那样, GC通过删除孤立对象来释放空间,以便它可以进一步分配。 在代码的末尾(垃圾收集发生的时间),再次((4))输出指针地址和年龄值。 这是我在我的机器上得到的输出(注意地址因机器而异,所以你的输出值不会相同):

    012CB4C8 100
    012A13D0 100
    
    链接地址: http://www.djcxy.com/p/13867.html

    上一篇: Do references get updated when Garbage Collectors move data in heap?

    下一篇: Which goes on the stack or heap?