由于对象是引用类型,它们存储在堆中,原始数据类型存储在堆栈中。
不完全是。 值类型,其中包括原语,但也有struct 类型在它们是本地时存储在堆栈中。如果装箱,它们也可以存储在堆中,或者存储在数组中,或者如您所见,存储在引用类型的字段中。
引用类型有一个或多个引用,这些引用也可能存储在堆栈中——你通过本地寻址它——以及对象本身在堆上的表示。
当作用域结束时,原始数据内存从堆栈中释放,但堆内存由垃圾收集器处理。
不完全是。
首先,没有真正的“释放”操作。假设我们在堆栈上使用 4 个插槽来存储值 1-4*:
[1][2][3][4][ ][ ][ ][ ]
^
Using up to here.
(为了简单起见,我将完全忽略函数调用之间发生的事情)。
现在假设我们停止使用最后 2 个插槽。没有必要“释放”任何东西:
[1][2][3][4][ ][ ][ ][ ]
^
Using up to here.
只有当我们去,例如使用 1 个新插槽来存储值 5,需要我们覆盖任何内容:
[1][2][5][4][ ][ ][ ][ ]
^
Using up to here.
“释放”只是改变了哪些内存被认为正在使用和哪些被认为可用。
现在考虑以下 C# 代码:
public void WriteOneMore(int num)
{
int result = num + 1;
Console.WriteLine(result);
}
假设您使用值42 调用它。堆栈的相关部分是:
[42]
^
Using up to here.
现在,int result = num + 1; 之后范围内有两个值; result 和 num。因此堆栈可能是:
[42][43]
^
Using up to here.
但是,num 再也不会被使用了。编译器和抖动知道这一点,所以他们可能重用了同一个槽:
[43]
^
Using up to here.
因为“在作用域内”是指源码,具体的地方可以使用哪些变量,但是栈是根据实际使用的是什么变量来使用的在特定的地方,所以它通常可以使用比源可能建议的更少的堆栈空间。相反,有时你会发现同一个变量变得不止一个槽,如果它以某种方式让编译器更容易的话。这在这里没什么大不了的,但是当我们涉及到引用类型时就变得很重要了。
堆内存由垃圾收集器处理。
让我们考虑一下这实际上意味着什么。
如果应用程序需要堆内存来存储新对象,它会从堆的空闲部分获取该内存。如果没有足够的可用堆内存,它可能会向操作系统请求更多,但在此之前它可能会尝试垃圾收集。
当这种情况发生时,垃圾收集器首先记下它无法删除的堆存储(引用类型,包括装箱值类型)对象。
其中一组对象位于static 变量中。
另一个是堆栈的可到达部分。所以如果栈是这样的:
["a"]["b"]["c"]["d"]["e"]
^
Using up to here.
那么"a"、"b"和"c"的值就不能被收集到了。
下一组是可以通过它已经知道无法收集的对象之一的字段或通过其中一个对象中的字段来访问的任何对象,依此类推。
(最后一步是由于上述原因不合格的任何对象,需要最终确定,它们被放入终结队列,因此在终结线程处理它们后它们将符合条件) .
现在。在堆上,对象看起来有点像;
[Sync][RTTI][Field0][Field1] … [FieldN]
这里的“同步”标记了锁定对象时使用的同步块。 “RTTI”标记了指向类型信息的指针,用于获取类型并使虚方法能够工作。剩下的就是字段,无论是直接包含的值类型还是对其他引用类型的引用。
好的。假设这个对象是收集器决定它可以收集的对象。
它只是将内存块从被认为不可用变为可用。就是这样。
在随后的步骤中,所有正在使用的对象将一起移动,以将已使用的内存压缩到一个块中,并将空闲的内存块压缩到另一个块中。我们的旧对象此时可能会被覆盖,或者在未来一段时间内可能不会被覆盖。我们真的不在乎,因为那个死物的尸体只是一堆 1 和 0 坐在那里什么都不做,等待再次写入易失性内存的最简写。
因此,原始字段在对象的内存被认为可以使用时被释放,但同样,它们可能仍然存在于 RAM 中一段时间,或者不存在,它们只是被忽略。
值得记住的是,就像堆栈上的值可能与源代码中的“范围内”不对应一样,因此可以在对象处于范围内时对其进行收集;垃圾收集取决于堆栈的实际使用情况,而不是来源。这基本上不会影响任何事情,因为大多数在代码中使用某些东西的尝试意味着它现在是堆栈实际使用的一部分,因此不会被收集。在极少数可能影响某些事情的情况中,最常见的可能是尝试使用仅通过本地引用的Timer;主线程不再使用它,以便可以用完堆栈空间,然后计时线程找不到这样的计时器。这就是GC.KeepAlive() 的用武之地。
*当涉及到正在运行的代码时,局部变量可能存储在寄存器中,而不是实际存储在内存堆栈中。在考虑 .NET 代码如何工作的层面上,通常最简单的方法是将它们也考虑“在堆栈上”。在考虑机器代码如何工作的层面上,这是不正确的。当垃圾收集器查看“堆栈上”的内容以查看它不能删除的内容时,它还会查看寄存器中的引用。