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