压缩发生后GC如何更新引用

.NET垃圾收集器收集对象(回收它们的内存)并执行内存压缩(以将内存碎片保持在最低限度)。

我想知道,因为一个应用程序可能有很多对象的引用,当对象的地址由于GC的压缩而改变时,GC(或CLR)如何管理这些对象的引用。

这个概念很简单,垃圾收集器只是更新任何对象引用并将它们重新指向移动的对象。

实现有点棘手,本机代码和托管代码之间没有真正的区别,它们都是机器代码。 并且对象引用没有什么特别之处,它只是运行时的指针。 所需要的是收集器找回这些指针并将其识别为引用托管对象的类型的可靠方法。 不仅要在压缩时指向对象移动时更新它们,还要识别确保对象不会过早收集的实时引用。

对于存储在GC堆上的类对象中存储的任何对象引用来说,这很简单,CLR知道对象的布局以及哪些字段存储指针。 存储在堆栈或cpu寄存器中的对象引用并不那么简单。 像局部变量和方法参数一样。

执行托管代码的关键属性使其与本机代码不同,CLR可以可靠地迭代托管代码所拥有的堆栈帧。 通过限制用于设置堆栈帧的代码类型来完成。 这在本机代码中通常是不可能的,“帧指针省略”优化选项特别令人讨厌。

堆栈帧首先走路让它找到存储在堆栈上的对象引用。 并且让它知道线程当前正在执行托管代码,以便也应该检查cpu寄存器的引用。 从托管代码到本机代码的转换涉及在收集器识别的堆栈上编写特殊的“cookie”。 因此它知道不应检查任何后续堆栈帧,因为它们将包含不引用托管对象的随机指针值。

启用非托管代码调试时,您可以在调试器中看到这一点。 查看Call Stack窗口并记下[Native to Managed Transition]和[Managed to Native Transition]注释。 那是调试器识别那些cookie。 这也很重要,因为它需要知道Locals窗口是否可以显示任何有意义的内容。 堆栈遍历也在框架中公开,请注意StackTrace和StackFrame类。 对于沙盒非常重要,代码访问安全性(CAS)执行堆栈遍历。

为简单起见,我将假设一个停止世界的GC,其中没有固定任何对象,每个对象在每个GC循环中被扫描和重新定位,并且没有任何目标与任何源重叠。 实际上,.NET GC有点复杂,但这应该可以很好地理解事物是如何工作的。

每次检查参考时,有三种可能性:

  1. 它是空的。 在这种情况下,不需要采取任何行动。

  2. 它标识一个对象,其标题表示它不是重定位标记(下面描述的一种特殊对象)。 在这种情况下,将对象移动到一个新位置,并用包含新位置的三字重定位标记替换原始对象,该对象的位置包含刚刚观察到的当前对象的引用 ,以及那个对象。 然后开始扫描新对象(系统可以忘记当前正在扫描的对象,因为它只是记录了它的地址)。

  3. 它标识一个对象,其标题表示它是重定位标记。 在这种情况下,更新正在扫描的引用以反映新地址。

一旦系统完成扫描当前对象,它就可以查看其旧位置,以便在开始扫描当前对象之前找出它正在做什么。

一旦对象被重新定位,其前三个单词的前一个内容将在其新位置可用,并且在旧位置将不再需要。 因为到对象的偏移量总是4的倍数,并且每个对象限制为2GB,所以只需要所有可能的32位值的一小部分来保存所有可能的偏移量。 如果对象标题中至少有一个单词至少有2 ^ 29个值,则除了对象重定位标记之外,它永远不能保存,并且如果每个对象至少分配了12个字节,则对象扫描可以处理任何树的深度,不需要在不再需要其内容的旧副本占用的空间之外存在任何与深度相关的存储。

垃圾收集

每个应用程序都有一组根。 Roots标识存储位置,这些位置引用托管堆上的对象或设置为null的对象。 例如,应用程序中的所有全局和静态对象指针都被视为应用程序根的一部分。 此外,线程堆栈上的任何局部变量/参数对象指针都被视为应用程序根的一部分。 最后,包含指向托管堆中对象的指针的任何CPU寄存器也被视为应用程序根的一部分。 活动根列表由实时(JIT)编译器和公共语言运行库维护,并且可供垃圾收集器的算法访问。

当垃圾收集器开始运行时,它假设堆中的所有对象都是垃圾。 换句话说,它假定应用程序的根中没有一个引用堆中的任何对象。 现在,垃圾收集器开始遍历根并构建可从根访问的所有对象的图形。 例如,垃圾收集器可以定位指向堆中对象的全局变量。

一旦图形的这一部分完成,垃圾收集器就会检查下一个根并再次遍历对象。 当垃圾收集器从一个对象走到另一个对象时,如果它试图将一个对象添加到它先前添加的图形中,那么垃圾收集器可以停止沿着该路径向下走。 这有两个目的。 首先,它有助于提高性能,因为它不会多次遍历一组对象。 其次,如果你有任何循环链接的对象列表,它可以防止无限循环。

一旦检查了所有根,垃圾收集器的图形就包含了从应用程序的根部以某种方式可以访问的所有对象的集合; 应用程序无法访问图形中不存在的任何对象,因此被视为垃圾。 垃圾收集器现在线性地遍历堆,寻找连续的垃圾对象块(现在被认为是自由空间)。 垃圾收集器然后将非垃圾对象向下移动到内存中(使用您已知多年的标准memcpy函数),删除堆中的所有间隙。 当然,在内存中移动对象会使指向对象的所有指针无效。 因此垃圾收集器必须修改应用程序的根,以便指针指向对象的新位置。 此外,如果任何对象包含指向另一个对象的指针,则垃圾收集器也负责更正这些指针。

C#固定语句

fixed语句设置一个指向托管变量的指针,并在执行语句时设置变量“pins”。 没有修复,指向可移动托管变量的指针几乎没用,因为垃圾收集可以无法预测地重定位变量。 C#编译器只允许您在固定语句中分配指向托管变量的指针。

垃圾收集:Microsoft .NET Framework中的自动内存管理

fixed语句(C#参考)