GC运行后对象会发生什么?

What happens to an object after GC runs?

考虑以下陈述

void foo()
{
    string text = "something";
    int a = 10;
    int b = a;

    var array = new int[5]{2,1,3,5,4};

    GC.Collect();
}



**Stack**           **Heap**

text = 0x20         0x20 something

a = 10

b = 10

array = 0x50:0x55   0x50 2
                    0x52 1
                    0x53 3
                    0x54 5
                    0x55 4

一些假设:

  1. GC.Collect() 在方法结束时被调用。
  2. GC 运行(假设)。

问题是:

  1. 堆中的值是多少?

    我回答内存位置0x20,0x50-0x55会被回收删除。他再次问道,内存位置有什么值?我猜可能是垃圾值。他说不对。 (WTx?)

  2. 堆栈中的项目会怎样?文本、a、b、数组持有什么值?

    我回答说,因为它们包含的引用已经被删除,它们也会被回收。 他问现在变量的值是多少。

  3. 在 GC 收集后重新绘制上面的表格列。

    GC运行后

    **Stack**           **Heap**
    
    text (cleared)      0x20 Garbage/NULL
    
    a (cleared) 
    
    b (cleared) 
    
    array (cleared)     0x50 Garbage/NULL
                        0x52 Garbage/NULL
                        0x53 Garbage/NULL
                        0x54 Garbage/NULL
                        0x55 Garbage/NULL
    

结果:他说答案不正确。任何想法我在这里错过了什么?

这是您无法回答的问题,因为它是您执行平台的实现细节。根据平台/版本的不同,答案可能完全不同。您可以考虑的是方法范围,值类型与引用类型以及字符串的特殊性。

不可能对此给出确定性的答案,但可以给出对行为的一些思考。

首先,所有的变量都是局部变量。因此, 方法返回后,这些变量超出范围。他们现在居住的内存(或 CPU 寄存器)中的确切位置包含未知内容。其他东西现在可能住在那个内存区域,我们不知道。 我们无法知道。因此,他们被清除了吗?谁知道呢

但是,就所有意图和目的而言,这些变量在方法返回后不再存在。

如果你认为"these variables live on the stack",please note that the stack is an implementation detail

现在,您另外分配了两个堆对象,一个字符串和一个数组。

由于这些仅由局部变量引用,在方法返回后超出范围,因此它们不再被视为有效并且符合收集条件。

不过,这里的"collection"是什么意思呢?是否意味着垃圾收集器清除了内存区域?完全没有。

相反,当垃圾回收内存时,它实际上做了相反的事情,它collects living objects。这些对象在内存中被压缩并四处移动,那个字符串和那个数组所在的区域可以被其他对象覆盖。可能是一个只包含零的数组,在这种情况下,内存区域可能看起来像 "cleared",但可能不是。

但是等等,那个字符串,它发生了一些特别的事情。由于该字符串是作为文字在源代码中编写的,因此在程序启动时它实际上是 interned 。因此,它不会 被收集,它将仍然 存在于内存中的某个地方。换句话说,该字符串在内存中的某个地方仍然存在并且完好。可能与变量引用它时所在的位置相同,也可能在其他地方。 我们无法知道

但是,垃圾收集器在压缩活动对象时可能会覆盖构成数组对象的字节。

好的,所有这些都发生在方法返回之后。

如果 GC.Collect() 在方法 returns 之前 运行垃圾回收周期会怎样?

好吧,如果您使用的是 RELEASE 版本,以上所有内容仍然有效。如果该变量不再在该方法中使用(即,您已经在执行时间线中传递了最后一次使用),那么您在方法中有一个引用堆上某物的局部变量这一事实毫无意义。

但是,如果您处于 DEBUG 版本中,或者附加了调试器,所有局部变量范围都会延长到方法的末尾。因此,它们仍然被视为活动引用,并且也会使堆上的对象保持活动状态。在这种情况下,数组将不会被收集。

但是,任何时候运行垃圾回收,堆对象在内存中的地址都可能发生变化,因为对象与其他对象一起压缩或提升到不同的代。


这里是您问题的实际答案:

以上大部分是实现细节。我们不应该关心的事情,因为它可能会因优化而改变。

有很多事情我们可以推理,但我们不应该如何处理实际的底层内存。

你在评论中说你认为你是在接受编译器作者的采访,我会说这种感觉是正确的。如果您要面试 Microsoft 的 JIT 或内存管理团队,至少会在工作中教授一些关于这些东西如何工作的知识,但如果您已经知道,这可能是一种奖励。

不过,对于一个普通的.NET程序员来说,这是没有必要关心的。其他聪明人关心这个。

对象以前占用的内存可能会发生以下三种情况之一:

  • 它被另一个对象覆盖了。当 GC 压缩堆时移动的一个。这是最有可能的结果。
  • 加入堆段的空闲链表,准备下次分配使用。通常在将它与免费 space 或块前后的分配合并之后,如果它们也被释放,这很常见。
  • 这是堆段中最后剩余的分配。该段可能会添加到未使用段池中,在需要新的 gen #0 段时重新使用。或者它的地址 space 可能会返回给操作系统。

大对象堆有点不同,第一个项目符号不适用。请注意 "memory locations are deleted" 的心理模型不是很准确。 "memory locations will be re-used" 你会遥遥领先。堆栈的情况大致相同,没有活动的 "delete the stack frame" 代码。它只是被遗忘了,迟早会被覆盖。几乎总是更快,微秒。