【问题标题】:Memory allocation: Stack vs Heap?内存分配:堆栈与堆?
【发布时间】:2011-05-28 02:26:22
【问题描述】:

我对堆栈与堆之间的内存分配基础知识感到困惑。根据标准定义(每个人都说),所有 Value Types 将被分配到 StackReference 类型将进入 强>堆

现在考虑以下示例:

class MyClass
{
    int myInt = 0;    
    string myString = "Something";
}

class Program
{
    static void Main(string[] args)
    {
       MyClass m = new MyClass();
    }
}

现在,c# 中的内存分配是如何发生的? MyClass(即m)的对象会完全分配给Heap吗?也就是说,int myIntstring myString 都会进入堆?

或者,对象将被分成两部分,并分配到两个内存位置,即堆栈和堆?

【问题讨论】:

标签: c# .net heap-memory stack-memory


【解决方案1】:

m 分配在堆上,其中包括myInt。在堆栈上分配原始类型(和结构)的情况是在方法调用期间,它为堆栈上的局部变量分配空间(因为它更快)。例如:

class MyClass
{
    int myInt = 0;

    string myString = "Something";

    void Foo(int x, int y) {
       int rv = x + y + myInt;
       myInt = 2^rv;
    }
}

rvxy 都将在堆栈上。 myInt 位于堆上的某个位置(并且必须通过 this 指针访问)。

【讨论】:

  • 一个重要的补充是要记住“堆栈”和“堆”实际上是 .NET 中的实现细节。完全可以创建一个完全不使用基于堆栈的分配的合法 C# 实现。
  • 我同意它们应该被处理,但它们纯粹是实现细节并不完全正确。它在公共 API 文档和语言标准(EMCA-334、ISO/IEC 23270:2006)中明确指出(即“结构值存储在‘堆栈上’。细心的程序员有时可以通过明智地使用结构来提高性能。 ") 但是,是的,如果堆分配的速度是您的应用程序的瓶颈,那么您可能做错了(或使用了错误的语言)。
【解决方案2】:

您应该将在哪里 对象分配的问题作为实现细节来考虑。对象的位存储在哪里对您来说并不重要。对象是引用类型还是值类型可能很重要,但在开始优化垃圾回收行为之前,您不必担心它的存储位置。

虽然在当前实现中引用类型总是在堆上分配,但值类型可能在堆栈上分配——但不一定。只有当值类型是未包含在引用类型中且未在寄存器中分配的未装箱非转义局部或临时变量时,才会在堆栈上分配值类型。

  • 如果值类型是类的一部分(如您的示例中所示),它将在堆上结束。
  • 如果它被装箱,它最终会在堆上。
  • 如果它在一个数组中,它将在堆上结束。
  • 如果它是一个静态变量,它将在堆上结束。
  • 如果它被闭包捕获,它将最终在堆上。
  • 如果在迭代器或异步块中使用它,它将最终在堆上。
  • 如果它是由不安全或非托管代码创建的,它可以分配在任何类型的数据结构中(不一定是堆栈或堆)。

有什么我错过的吗?

当然,如果我没有链接到 Eric Lippert 关于该主题的帖子,那我就失职了:

【讨论】:

  • Ed:到底什么时候重要?
  • @Gabe:比特的存储位置确实很重要。例如。如果您正在调试故障转储,除非您知道在哪里查找对象/数据,否则您不会走得太远。
  • 您错过的情况是:如果值类型来自通过不安全指针访问的非托管代码,那么它可能既不在堆栈上,也不在托管堆上。它可能在非托管堆上,或者在一些甚至不是堆的数据结构中。存在“堆”的整个想法也是一个神话。可以有几十个堆。此外,如果抖动选择注册该值,则它不在堆栈或堆中,而是在寄存器中。
  • Eric Lippert 的第二部分读起来很棒,感谢您提供链接!
  • 这很重要,因为它在面试中被问到,但在现实生活中却没有。 :)
【解决方案3】:

“所有 VALUE 类型都将分配给 Stack”是非常非常错误的;结构变量可以作为方法变量存在于堆栈中。但是,类型上的字段与该类型一起存在。如果字段的声明类型是类,则值作为该对象的 part 在堆上。如果一个字段的声明类型是一个结构体,那么这些字段就是该结构体所在的结构体的一部分。

即使方法变量可以在堆上,如果它们被捕获(lambda/anon-method),或者是(例如)迭代器块的一部分。

【讨论】:

  • 别忘了装箱:如果你在一个方法中有object x = 12;,即使它是一个整数(一个值类型),12 也会被存储在堆上。
  • @Gabe:值类型存储位置自身包含值类型的字段(公共和私有)。引用类型的存储位置要么持有null,要么持有对适当类型的堆对象的引用。对于每个值类型,都有一个对应的堆对象类型;尝试将值类型存储在引用类型存储位置将生成其对应的堆对象类型的新对象,将所有字段复制到该新对象,并将对该对象的引用存储在引用类型存储位置中。 C# 假装值类型和对象类型相同,但是...
  • ...这样的观点增加了混乱而不是理解。存储在该类型变量中的未装箱List<T>.Enumerator 将显示值语义,因为它是一个值类型。但是,存储在IEnumerator<T> 类型变量中的List<T>.Enumerator 的行为类似于引用类型。如果将后者视为与前者不同的类型,则行为上的差异很容易解释。假装他们是同一类型会更难推理。
【解决方案4】:
【解决方案5】:

简单的措施

值类型可以串在堆栈上,它是可以分配给一些未来主义数据结构的实现细节。

因此,最好了解值和引用类型的工作原理,值类型将按值复制,这​​意味着当您将值类型作为参数传递给 FUNCTION 时,它会被自然复制,这意味着您将拥有全新副本。

引用类型是通过引用传递的(在以后的某些版本中,再一次不要考虑引用会再次存储地址,它可能会存储在其他一些数据结构中。)

在你的情况下

myInt 是一个 int,它被封装在一个偏离引用类型的类中,因此它将与将存储在“堆”上的类的实例相关联。

我建议,你可以开始阅读 ERIC LIPPERTS 写的博客。

Eric's Blog

【讨论】:

    【解决方案6】:

    每次在其中创建对象时,它都会进入称为堆的内存区域。原始变量如 如果 int 和 double 是局部方法变量,则在堆栈中分配;如果它们是成员,则在堆中分配 变量。在方法中,当调用方法时,局部变量会被压入堆栈 当方法调用完成时堆栈指针递减。在多线程应用程序中,每个线程 将有自己的堆栈,但将共享同一个堆。这就是为什么在你的代码中应该小心避免任何 堆空间中的并发访问问题。堆栈是线程安全的(每个线程都有自己的堆栈),但是 除非通过您的代码进行同步保护,否则堆不是线程安全的。

    这个链接也有用http://www.programmerinterview.com/index.php/data-structures/difference-between-stack-and-heap/

    【讨论】:

      【解决方案7】:

      堆栈

      stack 是一块内存,用于存储local variablesparameters。堆栈在逻辑上随着函数的进入和退出而增长和缩小。

      考虑以下方法:

      public static int Factorial (int x)
      {
          if (x == 0) 
          {
              return 1;
          }
      
          return x * Factorial (x - 1);
      }
      

      这个方法是递归的,这意味着它会调用自己。 每次进入方法时,都会在堆栈上分配一个新的int每次方法退出时,都会释放该int


      • 堆是objects(即reference-type instances)所在的内存块。每当创建一个新对象时,它都会在堆上分配,并返回对该对象的引用。在程序执行期间,随着新对象的创建,堆开始填满。运行时有一个垃圾收集器,它会定期从堆中释放对象,因此您的程序不会运行Out Of Memory。只要一个对象没有被任何本身为 alive 的对象引用,它就有资格被释放。
      • 堆还存储static fields。与在堆上分配的对象(可以被垃圾回收)不同,these live until the application domain is torn down

      考虑以下方法:

      using System;
      using System.Text;
      
      class Test
      {
          public static void Main()
          {
              StringBuilder ref1 = new StringBuilder ("object1");
              Console.WriteLine (ref1);
              // The StringBuilder referenced by ref1 is now eligible for GC.
      
              StringBuilder ref2 = new StringBuilder ("object2");
              StringBuilder ref3 = ref2;
              // The StringBuilder referenced by ref2 is NOT yet eligible for GC.
              Console.WriteLine (ref3); // object2
          }
      }    
      

      在上面的例子中,我们首先创建了一个由变量 ref1 引用的 StringBuilder 对象,然后写出它的内容。然后该 StringBuilder 对象立即有资格进行垃圾收集,因为随后没有人使用它。然后,我们创建另一个由变量 ref2 引用的 StringBuilder,并将该引用复制到 ref3。即使 ref2 在那之后不再使用,ref3 也会保持同一个 StringBuilder 对象处于活动状态——确保在我们使用完 ref3 之前它没有资格被收集。

      值类型实例(和对象引用)存在于变量所在的任何位置 宣布。如果实例被声明为类类型中的字段或数组元素,则该实例位于堆上。

      【讨论】:

        【解决方案8】:

        m 是对 MyClass 对象的引用,因此 m 存储在主线程的堆栈中,但 MyClass 的对象存储在堆中。因此 myInt 和 myString 存储在堆中。 请注意, m 只是一个引用(内存地址)并且位于主堆栈上。当 m 被释放时,GC 从堆中清除 MyClass 对象 有关更多详细信息,请阅读本文的所有四个部分 https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

        【讨论】:

          【解决方案9】:

          根据标准定义(每个人都这么说),所有值类型都将分配到堆栈中,而引用类型将进入堆中。

          这是错误的。只有 local(在函数的上下文中)值类型/值类型数组在堆栈上分配。其他所有内容都在堆上分配。

          【讨论】:

          • primitives & structs 说起来有点毫无意义(尽管我没有对你投反对票)。只需删除primitives &,声明就会更清晰,准确度也不会降低。
          猜你喜欢
          • 2023-03-03
          • 2018-07-24
          • 1970-01-01
          • 2011-10-06
          • 1970-01-01
          • 2019-05-17
          • 2011-10-09
          • 1970-01-01
          相关资源
          最近更新 更多