【问题标题】:Are these objects's references on the Stack or on the Heap?这些对象的引用是在堆栈上还是在堆上?
【发布时间】:2011-02-03 07:04:50
【问题描述】:

如果有人能告诉我我是否理解得很好,我将不胜感激:

class X
{
   A a1=new A(); // reference on the stack, object value on the heap
   a1.VarA=5;    // on the stack - value type
   A a2=a1;      // reference on the stack, object value on the heap
   a2.VarA=10;   // on the stack - value type         
}

另外,a1a2 引用都在堆栈上,而它们的“对象”值在堆上。但是VarA 变量呢,它仍然是纯值类型?

class A
{
   int VarA;
}

【问题讨论】:

  • 由于这段代码没有编译,很难描述运行时如何处理它。所有这些语句都打算在方法体内吗?是字段声明还是局部变量声明?

标签: c# memory-management stack


【解决方案1】:

您正在询问有关实现细节的问题,因此答案将取决于特定的实现。让我们考虑一个实际编译的程序版本:

class A { public int VarA; }
class X
{
    static void Main(string[] args)
    {
        A a1 = new A();
        a1.VarA = 5;
        A a2 = a1;
        a2.VarA = 10;
    }
}

以下是在调试模式下运行 C# 4.0 的 Microsoft CLR 4.0 上发生的情况。

此时栈帧指针已被复制到寄存器ebp中:

这里我们为新对象分配堆内存。

A a1 = new A();
mov         ecx,382518h 
call        FFE6FD30 

返回对 eax 中堆对象的引用。我们将引用存储在堆栈槽 ebp-48 中,这是一个与任何名称无关的临时槽。请记住,a1 尚未初始化。

mov         dword ptr [ebp-48h],eax 

现在我们将刚刚存储在堆栈中的那个引用复制到 ecx 中,这将用于指向 ctor 调用的“this”指针。

mov         ecx,dword ptr [ebp-48h] 

现在我们调用ctor。

call        FFE8A518 

现在我们将存储在临时栈槽中的引用再次复制到寄存器 eax 中。

mov         eax,dword ptr [ebp-48h] 

现在我们将 eax 中的引用复制到栈槽 ebp-40 中,即 a1。

mov         dword ptr [ebp-40h],eax 

现在我们必须将 a1 提取到 eax 中:

a1.VarA = 5;
mov         eax,dword ptr [ebp-40h] 

请记住,eax 现在是 a1 所引用事物的堆分配数据的地址。那个东西的 VarA 字段是对象的四个字节,所以我们将 5 存储到那个:

mov         dword ptr [eax+4],5 

现在我们将 a1 的堆栈槽中的引用复制到 eax 中,然后将其复制到 a2 的堆栈槽中,即 ebp-44。

A a2 = a1;
mov         eax,dword ptr [ebp-40h] 
mov         dword ptr [ebp-44h],eax 

现在正如你所期望的那样,我们将 a2 放入 eax,然后将引用的四个字节写入 VarA:

a2.VarA = 10;
mov         eax,dword ptr [ebp-44h] 
mov         dword ptr [eax+4],0Ah

所以你的问题的答案是对对象的引用存储在堆栈中的三个位置:ebp-44、ebp-48 和 ebp-40。它们存储在 eax 和 ecx 的寄存器中。对象的内存(包括其字段)存储在托管堆上。这一切都在 Microsoft 的 CLR v4.0 的调试版本中的 x86 上。如果你想知道其他配置中的东西是如何存储在堆栈、堆和寄存器中的,那可能会完全不同。引用可以全部存储在堆中,也可以全部存储在寄存器中;可能根本没有堆栈。这完全取决于 jit 编译器的作者如何决定实现 IL 语义。

【讨论】:

  • 这还取决于 C# 编译器的作者如何决定实现 C# 语义。局部变量(a1a2)可以实现为托管类型中的字段,在每个堆栈帧中只留下一个引用。我意识到在您的帖子的评论中提出这一点会引起祖母和吸蛋的想法,但我想我还是会提到它:)
  • @Jon:确实。我们在编译器的 IL 生成阶段产生的错误非常少;其中之一是“本地人太多”——我不记得限制是什么,但这就像一个方法中的本地人或临时人员不能超过 32K 或 64K。 (显然真实的代码没有这个问题,但机器生成的代码可能。)我经常认为在这种情况下,我们应该而不是产生错误,而是开始将它们提升到字段。但是这个场景太模糊了,无法证明编写和测试代码的成本是合理的。
【解决方案2】:

严格来说,它是依赖于实现的。通常,.NET 开发人员不应该关心这些事情。据我所知,在 Microsoft 的 .NET 实现中,值类型的变量存储在堆栈中(当它们在方法中声明时),引用类型对象的数据在托管堆上分配。但是,请记住,当值类型是类的字段时,类数据本身存储在堆上(包括所有值类型字段)。 因此,不要将语义(值类型与引用类型)与分配规则混为一谈。这些东西可能相关,也可能不相关。

【讨论】:

    【解决方案3】:

    我想你可能有一点误解......

    一般来说,引用类型放在堆上,而我认为(可能是错误的)值类型/本地值放在堆栈上。但是,您的 A1.VarA 和 A2.VarA 示例指的是引用类型的字段 - 它与堆上的对象一起存储......

    【讨论】:

    • 是的,但是那个字段的值是int,所以是值类型,对吧?
    • @Petr,所有字段都包含在堆上的引用类型A中。
    【解决方案4】:

    在这种情况下,a1.VarA 将在堆上,因为当您执行 A a1 = new A() 时,它已经分配了空间。

    如果你只是在一个将进入堆栈的函数中执行int i = 5;,但正如你明确指出的那样,a1 将被分配在堆上,那么与之关联的所有值类型都将被放置在堆上

    【讨论】:

      【解决方案5】:
      class X 
      { 
          A a1=new A(); // reference on the stack, object value on the heap 
          a1.VarA=5;    // on the Heap- value type (Since it is inside a reference type)
          A a2=a1;      // reference on the stack, object value on the heap 
          a2.VarA=10;   // on the Heap - value type (Since it is inside a reference type)
      }
      

      【讨论】:

        【解决方案6】:

        阅读 Jeff Richter 的 CLR via C# 以全面了解该主题。

        【讨论】:

          【解决方案7】:

          记住在 C# 中深入阅读:- 只有局部变量(在方法内部声明的那个)和方法参数存在于堆栈中。在上述情况下,像 varA 这样的实例变量驻留在堆中。

          【讨论】:

          • 请注意,在 C# 的 Microsoft 实现中,作为 lambda 或匿名方法的封闭局部变量的局部变量不会存储在堆栈中。迭代器块中的局部变量也是如此。
          【解决方案8】:

          我也是 C# 新手。你的问题很重要,我也想到了。所有文档都说,值进入堆栈,引用进入堆,但正如上面的人所说,它只是用于方法内部的代码。在学习的阶梯上,我意识到所有程序代码都在属于堆实例的方法内部开始。所以从概念上讲,堆栈与堆并不相等,就像所有文档都使人们感到困惑。堆栈机制只能在方法中找到...

          【讨论】:

            猜你喜欢
            • 2016-12-27
            • 2013-12-09
            • 2013-03-07
            • 2010-11-06
            • 2019-12-02
            • 2012-07-28
            • 2011-04-08
            • 1970-01-01
            • 2016-03-06
            相关资源
            最近更新 更多