.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)执行堆栈遍历。
每个应用程序都有一组根。根标识存储位置,这些位置引用托管堆上的对象或设置为 null 的对象。例如,应用程序中所有全局和静态对象指针都被认为是应用程序的根。此外,线程堆栈上的任何局部变量 / 参数对象指针也被认为是应用程序的根。最后,包含指向托管堆中对象的指针的任何 CPU 寄存器也被认为是应用程序的根。活动根列表由即时 (JIT) 编译器和公共语言运行时维护,并被提供给垃圾回收算法。
当垃圾回收器开始运行时,它假设堆中的所有对象都是垃圾。换句话说,它假设应用程序的任何根都未引用堆中的任何对象。现在,垃圾回收器开始遍历根并构建从根可达到的所有对象的图形。例如,垃圾回收器可能会找到一个指向堆中对象的全局变量。
完成这部分图形后,垃圾回收器检查下一个根并再次遍历对象。随着垃圾回收器从对象到对象的遍历,如果它尝试将先前添加的对象添加到图形中,则垃圾回收器可以停止沿该路径向下遍历。这有两个目的。首先,它可以显著提高性能,因为它不会重复遍历一组对象。其次,它可以防止无限循环,如果您有任何循环对象列表。
一旦检查完所有根,垃圾回收器的图形包含从应用程序根的某种方式可达的所有对象集;不在图形中的任何对象都不可由应用程序访问,因此被视为垃圾。垃圾回收器现在线性地遍历堆,查找连续的垃圾对象块(现在被认为是空闲空间)。垃圾回收器然后将非垃圾对象向内存下移(使用您多年来所知道的标准 memcpy 函数),消除了堆中的所有间隙。当然,移动内存中的对象会使指向对象的所有指针失效。因此,垃圾回收器必须修改应用程序的根,以便指针指向对象的新位置。此外,如果任何对象包含指向另一个对象的指针,则垃圾回收器负责纠正这些指针。
fixed 语句将指针设置为托管变量并在语句执行期间 "固定" 该变量。没有 fixed,对可移动的托管变量的指针几乎没什么用处,因为垃圾回收可能会不可预测地重定位这些变量。C#编译器只允许您在 fixed 语句中分配指针给托管变量。