【问题标题】:How are reference types cleared from memory?如何从内存中清除引用类型?
【发布时间】:2015-11-21 14:20:21
【问题描述】:

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

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

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

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

【问题讨论】:

  • 我建议你阅读 Eric Lippert 的this 系列文章。很多人都沉迷于引用类型 = 堆分配,值类型 = 堆栈分配。
  • @madan Code formatting 仅用于代码,而非强调。无论如何,强调通常是不必要的,尤其是在像你这样的情况下。

标签: c# .net memory heap-memory


【解决方案1】:

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

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

注意:以下部分根据关于“原始类型”术语的评论对话进行了轻微编辑:

(编辑) 在这个问题的上下文中,谈论“值类型”而不是“原始类型”更合适。不管类型是不是原始类型,在这种情况下,重要的是它是值类型还是引用类型。 (结束编辑)

现在重点:

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

样品:

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

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

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

【讨论】:

  • “原始类型”这个表达在ECMA-335中出现了很多次,所以.NET中肯定有这样的东西。在 ECMA-334 中,“原始”仅出现在引号中,因此没有定义为“原始”,但这个概念当然不是不适用的。
  • 表达式“原始类型”在 ECMA-335 (ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf) 中出现一次,既不是定义也不是主题标题。 “值类型”出现了 379 次,多次作为主题标题和定义实体出现。决定使用它是否合适,还要决定 .NET 中是否有任何与 Java 基本类型相似的东西,然后决定在 .NET 的上下文中使用这个术语是否会产生误导,尤其是当你想解释的时候一些基础架构的东西。
  • 我的计数中还包括“原始和非原始值类型”、“原始或值类型”、“原始数字类型”、“原始数字类型”。在这种情况下,原语和其他值类型之间没有区别,但你不能说 .NET 中没有这种东西。
  • @Jon:好的,我理解你的意思。另外,我们同意关于“这种情况没有区别”。我的观点是:那里有许多令人困惑的“文本”,其中大多数都因使用不恰当的术语而错过了重点(或至少是模糊的)。 (加上:“内置类型”(我最喜欢的:-))。
【解决方案2】:

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

不完全是。 值类型,其中包括原语,但也有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; 之后范围内有两个值; resultnum。因此堆栈可能是:

[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 代码如何工作的层面上,通常最简单的方法是将它们也考虑“在堆栈上”。在考虑机器代码如何工作的层面上,这是不正确的。当垃圾收集器查看“堆栈上”的内容以查看它不能删除的内容时,它还会查看寄存器中的引用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-03-11
    • 2019-05-08
    • 2012-01-20
    • 1970-01-01
    • 2015-11-21
    • 2011-05-10
    • 1970-01-01
    • 2018-08-11
    相关资源
    最近更新 更多