内存分配:栈 vs 堆?

89

我对栈(stack)堆(heap)的内存分配基础概念感到困惑。按照标准定义(大家都说的那些东西),所有值类型(Value Types)都将被分配到栈(Stack)上,而引用类型(Reference Types)将放到堆(Heap)中。

现在考虑以下示例:

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

class Program
{
    static void Main(string[] args)
    {
       MyClass m = new MyClass();
    }
}
现在,在C#中内存分配会如何发生?我的类(即m)的对象是否将完全分配给堆?也就是说,int myInt和string myString都会进入堆吗? 或者,对象将被分成两部分,并将分配到堆和栈两个内存位置?

这回答了你的问题吗?栈和堆是什么,它们在哪里? - user12031933
9个回答

76

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

尽管当前实现中,引用类型总是在堆上分配,但值类型可以在堆栈上分配-但不一定。只有当值类型是未装箱的非逃逸本地或临时变量,并且不包含在引用类型中并且未在寄存器中分配时,才会在堆栈上分配值类型。

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

我有漏掉的吗?

当然,如果我没有提供以下关于此主题的Eric Lippert的帖子,那将是我的疏忽:

  • 也许最好的一篇是:关于值类型的真相

  • 1
    Ed:到底什么时候才重要呢? - Gabe
    1
    @Gabe:位的存储位置确实很重要。例如,如果您正在调试崩溃转储,除非您知道在哪里查找对象/数据,否则您将无法取得太大进展。 - Brian Rasmussen
    16
    你错过的情况是:如果值类型来自通过不安全指针访问的非托管代码,则可能既不在堆栈上也不在托管堆上。它可能在非托管堆上,或者在某些甚至不是堆的数据结构中。"堆"存在的整个想法也是一个神话。可能会有数十个堆。此外,如果Jitter选择将该值存储在寄存器中,则它不在堆栈或堆中,而在寄存器中。 - Eric Lippert
    1
    Eric Lippert的第二部分是一篇非常棒的阅读,感谢您提供链接! - Dan Bechard
    3
    这很重要,因为这个问题在面试中会被问到,但在现实生活中却不会。 :) - Mayank
    显示剩余13条评论

    62

    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
    需要翻译的内容:An important addendum is to remember that "the stack" and "the heap" are really implementation details in .NET. It's perfectly possible to create a legal implementation of C# that doesn't use stack-based allocation at all.重要的附言是要记住,“堆栈”和“堆”实际上是.NET中的实现细节。 完全可以创建一个合法的C#实现,完全不使用基于堆栈的分配。 - JSBձոգչ
    5
    我同意它们应该被“当作”如此处理,但并不完全正确地认为它们仅仅是实现细节。它在公共API文档和语言标准(EMCA-334,ISO/IEC 23270:2006)中明确说明(即,“结构体的值存储在'堆栈'上。小心的程序员有时可以通过谨慎使用结构体来提高性能。”),但是,如果堆分配的速度成为应用程序的瓶颈,那么你可能正在做错事情(或者使用了错误的语言)。 - Mud

    25
    "

    所有的值类型都会被分配到堆栈上"是非常错误的;结构体变量可以作为方法变量存放在堆栈中。然而,类型的字段与该类型一起存在。如果字段的声明类型是类,则值作为该对象的一部分在堆上。如果字段的声明类型是结构体,则字段作为该结构体的一部分存储在该结构体所在的位置。

    即使方法变量也可能在堆上,如果它们被捕获(lambda/匿名方法)或者是一个迭代器块的一部分。

    "

    1
    不要忘记装箱:如果你在一个方法中有object x = 12;,即使它是一个值类型(整数),这个12也会被存储在堆上。 - Gabe
    @Gabe:值类型存储位置在其内部保存值类型的字段(公共和私有)。引用类型存储位置可以保存null,或适当类型的堆对象的引用。对于每个值类型,都有一个相应的堆对象类型;尝试将值类型存储在引用类型存储位置中将产生一个新对象,该对象属于其相应的堆对象类型,将所有字段复制到该新对象中,并将对象的引用存储在引用类型存储位置中。C#假装值类型和对象类型是相同的,但是... - supercat
    这样的观点会增加混淆而非理解。存储在该类型变量中的未装箱的 List<T>.Enumerator 将表现出值语义,因为它是值类型。然而,存储在 IEnumerator<T> 类型变量中的 List<T>.Enumerator 将表现得像引用类型。如果将后者视为与前者不同的类型,则行为差异很容易解释。假装它们是相同类型会使推理变得更加困难。 - supercat

    12

    优秀的解释:

    这些文章介绍了C#中的内存管理,包括垃圾收集器、对象生命周期和内存泄漏等主题。这些内容对于开发人员来说非常重要,因为它们可以帮助开发人员编写更高效、更稳定的代码。

    这对我来说是最好的答案 :) - user2088260

    3

    堆栈

    堆栈是用于存储局部变量参数的一块内存块。随着函数的进入和退出,堆栈在逻辑上增长和缩小。

    考虑以下方法:

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

    这个方法是递归的,意味着它会调用自身。每次进入该方法时,在堆栈上分配一个新的int,并在每次退出该方法时释放该int。
    堆是一块存储对象(即引用类型实例)的内存块。每当创建新对象时,它就会被分配到堆中,并返回对该对象的引用。在程序执行期间,随着新对象的创建,堆开始填充。运行时有一个垃圾回收器,定期从堆中回收对象,因此您的程序不会出现内存不足的情况。只要某个对象没有被任何“活着”的东西引用,它就有资格被释放。
    堆还存储静态字段。与在堆上分配的对象不同(可以被垃圾回收),这些字段会一直存在,直到应用程序域被关闭。
    考虑以下方法:
    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

    简单措施

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

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

    引用类型通过引用传递(再次不考虑引用将在某些未来版本中再次存储地址,它可能存储在其他数据结构中)。

    所以在你的情况下

    myInt是一个int,它封装在一个类中,当然是一个引用类型,因此它将绑定到存储在“堆”上的类的实例。

    我建议您开始阅读埃里克·利珀特(ERIC LIPPERTS)撰写的博客。

    Eric's Blog


    1
    每次创建对象时,它都会进入称为堆的内存区域。原始变量(如int和double)分配在堆栈中,如果它们是局部方法变量,则分配在堆中,如果它们是成员变量,则分配在堆中。在方法中,当调用方法时,本地变量将被推入堆栈中,并在完成方法调用时减少堆栈指针。在多线程应用程序中,每个线程将拥有自己的堆栈,但将共享相同的堆。这就是为什么在代码中应该注意避免任何并发访问堆空间的问题。堆栈是线程安全的(每个线程将拥有自己的堆栈),但除非通过代码同步进行保护,否则堆不是线程安全的。
    这个链接也很有用 http://www.programmerinterview.com/index.php/data-structures/difference-between-stack-and-heap/

    0

    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/


    -1
    根据标准定义(大家都说的那种),所有值类型都会被分配到堆栈上,而引用类型则会进入堆中。
    这是错误的。只有函数上下文中的本地值类型/值类型数组会被分配到堆栈上。其他所有东西都会分配到堆中。

    “primitives & structs” 这样说有点无意义(虽然我没有给你投反对票)。只需删除 “primitives &”,这个陈述就更清晰了,而且也不会不准确。 - mjwills

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接