如何从内存中清除引用类型?

How are reference types cleared from memory?

由于对象是引用类型,它们存储在堆中,原始数据类型存储在堆栈中。

但是一个对象是原始数据类型和引用类型的集合,即一个对象可能有一个整数数据成员and/or可能有另一个对象在其中。

当作用域结束时,原始数据内存从堆栈中释放,但堆内存由垃圾收集器处理。

现在我的问题是:如果一个对象也有原始数据成员,那么它们什么时候被删除?

很难解释这样一个基本但并不总是容易理解的东西。然而在过去的 15 年里写了很多很好的解释。

如果你不想阅读它们(显然......)这里有一个非常简短(因此不完整)的总结:(注意:我仍然强烈建议在文献中进行调查)

注意:以下部分根据有关 "primitive type" 术语的评论对话略作编辑:

(编辑) 在这个问题的上下文中,谈论 "value type" 而不是 "primitive type" 更合适。不管类型是不是原始类型,重要的是在此上下文中它是值类型还是引用类型。 (结束编辑)

现在重点:

引用类型有一个引用(任何地方,比如在堆或堆栈中)指向堆上分配的实例总是。值类型立即存储(任何地方,如在堆或堆栈中)嵌入那个地方,所以没有间接。

样本:

  • 值类型的局部变量:栈
  • 引用类型的局部变量:实例本身在堆上,引用在栈上
  • 成员变量(值类型):嵌入到实例的分配space中,它是成员变量。
  • 成员变量(引用类型):它的引用嵌入到它是成员变量的实例的分配space中,and its 实例在堆上。

Now my question is: if an object also has a primitive data member then when are they removed?

答案:当包含对象被移除时。 (希望基于 4 个示例可以清楚:包含对象可以在堆上或堆栈上,因此 "containing object removal" 可以是 GC 集合或从方法返回时设置的简单堆栈指针。)

As objects are reference types they are stored in the heap and primitive data types are store on the stack.

不完全是。 值类型,其中包括原语,而且 struct 类型在本地时存储在堆栈中。如果装箱,它们也可以存储在堆上,或者存储在数组中,或者如您所说,存储在引用类型的字段中。

引用类型有一个或多个引用,这些引用也可能存储在堆栈上——您通过它寻址的本地——以及对象本身在堆上的表示。

When the scope ends the primitive data memory is released from the stack but the heap memory is handled by the garbage collector.

不完全是。

首先,实际上并没有 "releasing" 操作。假设我们在堆栈上使用 4 个槽来存储值 1-4*:

[1][2][3][4][ ][ ][ ][ ]
          ^
       Using up to here.

(为了简单起见,我将完全忽略函数调用之间发生的事情)。

现在假设我们停止使用最后 2 个插槽。不需要 "release" 任何东西:

[1][2][3][4][ ][ ][ ][ ]
    ^
  Using up to here.

只有我们去,例如使用 1 个新插槽来存储值 5,我们需要覆盖任何内容吗:

[1][2][5][4][ ][ ][ ][ ]
       ^
     Using up to here.

"releasing" 刚刚更改了哪些内存被视为正在使用,哪些内存被视为可用。

现在考虑以下 C# 代码:

public void WriteOneMore(int num)
{
  int result = num + 1;
  Console.WriteLine(result);
}

假设您用值 42 调用它。堆栈的相关部分是:

[42]
 ^
 Using up to here.

现在,在 int result = num + 1; 之后范围内有两个值; resultnum。因此堆栈可能是:

[42][43]
     ^
   Using up to here.

然而,num 再也没有被使用过。编译器和抖动知道这一点,所以它们可能重用了同一个槽:

[43]
 ^
 Using up to here.

因为"in scope"指的是源码,具体的地方可以使用什么变量,但是栈是根据实际使用什么变量are 在特定的地方使用,所以它通常可以使用比源建议的更少的堆栈 space。相反,有时您会发现同一个变量变成多个槽,如果它在某种程度上使编译器更容易的话。这在这里没什么大不了的,但是当我们涉及到引用类型时就变得很重要了。

the heap memory is handled by the garbage collector.

让我们考虑一下这到底意味着什么。

如果应用程序需要用于新对象的堆内存,它会从堆的空闲部分获取该内存。如果没有足够的可用堆内存,它可以向 OS 请求更多,但在此之前它可能会尝试垃圾收集。

发生这种情况时,垃圾收集器首先记录下它无法 删除哪些堆存储(包括盒装值类型的引用类型)对象。

一组这样的对象是那些在 static 变量中的对象。

另一个是那些在堆栈的可达部分。所以如果堆栈是这样的:

["a"]["b"]["c"]["d"]["e"]
            ^
          Using up to here.

"a""b""c"等值无法采集。

下一组是可以通过已知无法收集的对象之一的字段或通过其中一个对象的字段到达的任何对象,依此类推。

(最后一步是任何由于上述原因不合格的对象,需要被终结,他们在这里被放入终结队列,所以他们将在终结线程处理后符合条件他们)。

现在。在堆上,对象有点像;

[Sync][RTTI][Field0][Field1]  … [FieldN]

此处"Sync"标记锁定对象时使用的同步块。 "RTTI" 标记指向类型信息的指针,用于获取类型和启用虚方法。其余的是字段,无论是直接包含值类型还是对其他引用类型的引用。

好的。假设这个对象是收集器决定它可以收集的对象。

它只是将该内存块从被认为不可用更改为可用。就是这样。

在随后的步骤中,所有正在使用的对象一起移动,将已用内存压缩到一个块中,将空闲内存压缩到另一个块中。我们的旧对象此时可能会被覆盖,也可能在未来一段时间内不会被覆盖。我们真的不在乎,因为那个死对象的尸体只是一堆 1 和 0 坐在那里什么都不做,等待再次写入易失性内存的重写本。

所以原始字段在对象的内存被认为可以使用时被释放,但是同样,它们可能仍然存在于 RAM 中一段时间​​,或者不存在,它们只是被忽略。

值得记住的是,正如堆栈上的值可能与源代码中的 "in scope" 不对应一样,因此可以在范围内收集对象;垃圾收集取决于堆栈的实际使用情况,而不是来源。这基本上不会影响任何东西,因为大多数尝试在代码中使用某些东西意味着它现在是堆栈实际使用的一部分,因此不会被收集。在极少数可能影响某些事物的情况下,最常见的可能是尝试使用仅通过本地引用的 Timer;主线程不再使用它,所以堆栈 space 可以用完,然后计时线程找不到这样的计时器。这就是 GC.KeepAlive() 的用武之地。

*当谈到 运行 代码时,局部变量可能存储在寄存器中,而实际上从未存储在内存堆栈中。在考虑 .NET 代码如何工作的层面上,通常最简单的方法就是同时考虑它们 "on the stack"。在考虑机器代码如何工作的层面上,事实并非如此。当垃圾收集器查看什么是 "on the stack" 以查看它不能删除的内容时,它还会查看寄存器中的引用。