内存分配:堆栈还是堆?


83

我对Stack vs Heap之间的内存分配基础感到困惑。根据标准定义(每个人都说),所有值类型都将分配到堆栈上,引用类型将放入堆中

现在考虑以下示例:

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

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

现在,如何在c#中进行内存分配?MyClassm)的对象会完全分配给堆吗?也就是说,int myIntstring myString既会去堆?

还是将对象分为两部分,并分配给两个内存位置,即堆栈和堆?


我之所以投票是因为即使这些长期存在的愚蠢陈述是错误的,我相信也会有一些好的答案。但是,针对建议的“双重分配”提出微不足道的反论点是很容易的(提示:类对象可以而且很多时候都可以跨越函数调用边界)。

这回答了你的问题了吗?什么是堆栈和堆?
Olivier Rogier

Answers:


55

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指针进行访问)。


7
一个重要的附录是要记住,“堆栈”和“堆”实际上是.NET中的实现细节。完全可以创建完全不使用基于堆栈的分配的C#合法实现。
JSBձոգչ2010年

5
我同意应该以这种方式对待它们,但是它们纯粹是实现细节并不是完全正确的。在公共API文档和语言标准(EMCA-334,ISO / IEC 23270:2006)中已明确指出(即“结构值存储在堆栈上”。谨慎的程序员有时可以通过明智地使用结构来提高性能。 “)但是,是的,如果堆分配的速度成为应用程序的瓶颈,那么您可能做错了(或使用了错误的语言)。
泥2010年

65

您应该考虑将对象分配在哪里作为实现细节的问题。精确地将对象的位存储在哪里对您来说都没有关系。一个对象是引用类型还是值类型可能很重要,但是您不必担心对象的存储位置,直到您开始优化垃圾回收行为为止。

尽管在当前的实现中,引用类型总是分配在堆上,但是值类型可以分配在堆栈上,但不一定。仅当值类型是未装箱的,未转义的局部或临时变量且未包含在引用类型中且未在寄存器中分配时,才在堆栈上分配值类型。

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

我有什么想念的吗?

当然,如果我不链接到埃里克·利珀特(Eric Lippert)关于该主题的帖子,那我将是失职的:


1
埃德:到底什么时候重要?
加布

1
@Gabe:位存储在哪里并不重要。例如,如果您要调试故障转储,除非您不知道在哪里查找对象/数据,否则您将走得很远。
Brian Rasmussen

14
您错过的情况是:如果值类型来自通过不安全指针访问的非托管代码,那么它可能既不在堆栈上,也不在托管堆上。它可能在非托管堆上,或者甚至在不是堆的某些数据结构中。存在“堆”的整个想法也是一个神话。可能有数十堆。同样,如果抖动选择注册该值,那么它不在堆栈或堆中,而是在寄存器中。
埃里克·利珀特

1
埃里克·利珀特(Eric Lippert)的第二部分是一篇精彩的读物,谢谢您的链接!
丹·贝查德

1
这很重要,因为它是在访谈中提出的,而不是现实生活中提出的。:)
Mayank

22

“所有的VALUE类型都会分配给堆栈”是非常非常错误的;结构变量可以作为方法变量存在于堆栈中。但是,类型上的字段与该类型一起使用。如果字段的声明类型是类,则这些值作为该对象的一部分在堆上。如果字段的声明类型是结构,则无论该结构存在于何处,这些字段都是该结构的一部分。

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


1
而且不要忘了装箱:如果您有object x = 12;方法,即使它是整数(值类型),也将12存储在堆中。
加布

@Gabe:值类型存储位置将值类型的字段(公共字段和私有字段)保存在其内部。引用类型的存储位置为holdnull或对适当类型的堆对象的引用。对于每种值类型,都有一个对应的堆对象类型。尝试将值类型存储在引用类型存储位置中将产生一个与之对应的堆对象类型的新对象,将所有字段复制到该新对象,并将对该对象的引用存储在引用类型存储位置中。C#假装值类型和对象类型相同,但是……
supercat 2012年

...这样的观点增加了混乱而不是理解。List<T>.Enumerator存储在该类型变量中的取消装箱将显示值语义,因为它是值类型。List<T>.Enumerator存储在type变量中的A的IEnumerator<T>行为类似于引用类型。如果人们将后者视为与前者不同的类型,那么行为上的差异就很容易解释。假装它们是同一类型,就很难对它们进行推理。
2012年

12

2

叠放

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之前,它不符合收集条件。

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


1

简单措施

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

因此,最好了解值和引用类型是如何工作的,值类型将按值复制,这​​意味着当您将值类型作为参数传递给FUNCTION时,比按自然方式将其复制意味着您将拥有一个总的新副本。

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

所以你的情况

myInt是一个int,它封装在一个类中,该类偏离了引用类型,因此它将与将存储在“ THE HEAP”上的类的实例绑定。

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

埃里克的博客


1

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

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.