.NET 垃圾收集器收集对象(回收它们的内存)并执行内存压缩(将内存碎片保持在最低限度)。
我想知道,由于应用程序可能对对象有很多引用,当对象的地址由于 GC 进行压缩而发生变化时,GC(或 CLR)如何管理对对象的这些引用。
.NET 垃圾收集器收集对象(回收它们的内存)并执行内存压缩(将内存碎片保持在最低限度)。
我想知道,由于应用程序可能对对象有很多引用,当对象的地址由于 GC 进行压缩而发生变化时,GC(或 CLR)如何管理对对象的这些引用。
这个概念很简单,垃圾收集器只是更新任何对象引用并将它们重新指向移动的对象。
实现有点棘手,本机代码和托管代码之间没有真正的区别,它们都是机器代码。对象引用并没有什么特别之处,它只是运行时的指针。收集器需要一种可靠的方法来找到这些指针并将它们识别为引用托管对象的类型。不仅要在压缩时移动指向的对象时更新它们,还要识别实时引用以确保不会过早收集对象。
对于存储在 GC 堆上的类对象中存储的任何对象引用,这很简单,CLR 知道对象的布局以及哪些字段存储指针。对于存储在堆栈或 cpu 寄存器中的对象引用来说,这并不是那么简单。像局部变量和方法参数一样。
执行托管代码与本机代码不同的关键属性是 CLR 可以可靠地迭代托管代码拥有的堆栈帧。通过限制用于设置堆栈帧的代码类型来完成。这在本机代码中通常是不可能的,“帧指针省略”优化选项特别讨厌。
堆栈帧遍历首先让它找到存储在堆栈中的对象引用。并让它知道线程当前正在执行托管代码,因此也应该检查 cpu 寄存器的引用。从托管代码到本机代码的转换涉及在收集器识别的堆栈上编写一个特殊的“cookie”。因此它知道不应检查任何后续堆栈帧,因为它们将包含从未引用托管对象的随机指针值。
当您启用非托管代码调试时,您可以在调试器中看到这一点。查看调用堆栈窗口并注意 [Native to Managed Transition] 和 [Managed to Native Transition] 注释。那就是识别这些cookie的调试器。对它也很重要,因为它需要知道 Locals 窗口是否可以显示任何有意义的内容。堆栈遍历也在框架中公开,请注意 StackTrace 和 StackFrame 类。并且对于沙盒非常重要,代码访问安全性 (CAS) 执行堆栈遍历。
为简单起见,我将假设一个 stop-the-world GC,其中没有对象被固定,每个对象在每个 GC 周期都被扫描和重定位,并且没有一个目标与任何源重叠。实际上,.NET GC 有点复杂,但这应该可以很好地了解事情的工作方式。
每次检查参考文献时,都有三种可能性:
它是空的。在这种情况下,不需要任何操作。
它标识一个对象,其标题表明它不是重定位标记(下面描述的一种特殊对象)。在这种情况下,将对象移动到新位置并用包含新位置的三字重定位标记替换原始对象,该对象的旧位置包含对当前对象的刚刚观察到的引用,以及其中的偏移量那个物体。然后开始扫描新对象(系统可以暂时忘记正在扫描的对象,因为它只是记录了它的地址)。
它标识一个对象,其标头说它是一个重定位标记。在这种情况下,请更新正在扫描的引用以反映新地址。
一旦系统完成扫描当前对象,它可以查看它的旧位置以找出它在开始扫描当前对象之前正在做什么。
一旦一个对象被重新定位,它的前三个单词的先前内容将在其新位置可用,而在旧位置将不再需要。因为一个对象的偏移量总是四的倍数,并且单个对象每个限制为 2GB,所以只需要所有可能的 32 位值的一小部分来保存所有可能的偏移量。假设一个对象头中的至少一个词至少有 2^29 个值,除了对象重定位标记之外,它永远不能保存任何值,并且假设每个对象都分配了至少 12 个字节,对象扫描可以处理任何树的深度,而不需要在不再需要其内容的对象的旧副本所占用的空间之外的任何深度相关存储。
每个应用程序都有一组根。根标识存储位置,这些位置引用托管堆上的对象或设置为空的对象。例如,应用程序中的所有全局和静态对象指针都被视为应用程序根的一部分。此外,线程堆栈上的任何局部变量/参数对象指针都被视为应用程序根的一部分。最后,任何包含指向托管堆中对象的指针的 CPU 寄存器也被视为应用程序根的一部分。活动根列表由即时 (JIT) 编译器和公共语言运行时维护,并且可供垃圾收集器的算法访问。
当垃圾收集器开始运行时,它会假设堆中的所有对象都是垃圾。换句话说,它假定应用程序的根都没有引用堆中的任何对象。现在,垃圾收集器开始遍历根并构建从根可到达的所有对象的图。例如,垃圾收集器可能会定位一个指向堆中对象的全局变量。
一旦图的这一部分完成,垃圾收集器检查下一个根并再次遍历对象。当垃圾收集器从一个对象到另一个对象时,如果它试图将一个对象添加到它之前添加的图表中,那么垃圾收集器可以停止沿着这条路径走。这有两个目的。首先,它显着提高了性能,因为它不会多次遍历一组对象。其次,如果您有任何对象的循环链表,它可以防止无限循环。
一旦检查了所有的根,垃圾收集器的图就会包含从应用程序的根以某种方式可以到达的所有对象的集合;任何不在图中的对象都不能被应用程序访问,因此被视为垃圾。垃圾收集器现在线性地遍历堆,寻找连续的垃圾对象块(现在被认为是空闲空间)。然后垃圾收集器将非垃圾对象向下移动到内存中(使用您多年来知道的标准 memcpy 函数),消除堆中的所有间隙。当然,移动内存中的对象会使所有指向对象的指针无效。所以垃圾收集器必须修改应用程序的根,以便指针指向对象的新位置。此外,如果任何对象包含指向另一个对象的指针,垃圾收集器也负责纠正这些指针。
固定语句设置一个指向托管变量的指针,并在语句执行期间“固定”该变量。如果没有固定,指向可移动托管变量的指针将几乎没有用处,因为垃圾收集可能会不可预测地重新定位变量。C# 编译器仅允许您在固定语句中分配指向托管变量的指针。