天天看点

C#引用类型和值类型在堆、栈中的存储

一、栈和堆是什么

程序运行时,它的数据必须存储在内存中。一个数据项需要多大的内存、存储在什么地方、以及如何存储都依赖于该数据项的类型。

运行中的程序使用两个内存区域来存储数据:栈和堆。

1、栈

栈是一个内存数组,是一个LIFO(last-in first-out,后进先出)的数据结构。栈存储几种类型的数据:某些类型变量的值、程序当前的执行环境、传递给方法的参数。

栈的特点:(1)数据只能从栈的顶端插入和删除。(2)把数据放到栈顶称为入栈。(3)从栈顶删除数据称为出栈。

C#引用类型和值类型在堆、栈中的存储

2、堆

堆是一块内存区域,在堆里可以分配大块的内存用于存储某种类型的数据对象。与栈不同,堆里的内存能够以任意顺序存入和移除。下图展示了一个在堆里放了4项数据的程序。

C#引用类型和值类型在堆、栈中的存储

虽然程序可以在堆里保存数据,但并不能显式地删除它们。CLR的自动垃圾收集器在判断出程序的代码将不会再访问某数据项时,会自动清除无主的堆对象。下图阐明了垃圾收集过程。

C#引用类型和值类型在堆、栈中的存储

二、值类型和引用类型

C#中的值类型和引用类型大致如下图所示:

C#引用类型和值类型在堆、栈中的存储

程序的数据项的类型定义了存储数据需要的内存大小以及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。

值类型与引用类型的存储方式:

值类型:值类型只需要一段单独的内存,用于存储实际的数据。

引用类型:引用类型需要两段内存,第一段存储实际的数据,它总是位于堆中。第二段是一个引用,指向数据在堆中的存放位置。

C#引用类型和值类型在堆、栈中的存储

问题:如下代码所示,我们都说值类型存在于栈中,引用类型的实际数据在栈中,引用存在堆中。那么下面的代码,当实例化 Student stu=new Student()时,那么Age属于值类型会存在于栈中吗?StudentAddress属于引用类型,会在栈和堆之间分成两半吗?答案是否定的。

public class Student
{ 
    public int Age { get; set; }
    public string Name { get; set; }
    public Address StudentAddress { get; set; }
}
public class Address
{

   public string address { get; set; }
}
           

请记住,对于一个引用类型,其实例的数据部分始终存放在堆里。既然两个成员都是对象数据的一部分,那么它们都会被存放在堆里,无论它们是值类型还是引用类型。如下图所示:

C#引用类型和值类型在堆、栈中的存储

尽管成员Age是值类型,但它也是new Student()实例数据的一部分,因此和对象的数据一起被存放在堆里。

成员StudentAddress是引用类型,所以它的数据部分会始终存放在堆里,正如图中“数据”框所示。不同的是,它的引用部分也被存放在堆里,封装在new Student()对象的数据部分中。

说明:对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。

三、申明变量时,内存是如何变化的?

当你在一个.NET应用程序中定义一个变量时,在RAM中会为其分配一些内存块。这块内存有三样东西:变量的名称、变量的数据类型以及变量的值。

在.NET中有两种可分配的内存:栈和堆。你的变量究竟会被分配到哪种类型的内存取决于数据类型。在接下来的几个部分中,详细地说明这两种类型的存储。

C#引用类型和值类型在堆、栈中的存储

Line 1:当这一行被执行后,编译器会在栈上分配一小块内存。栈会在负责跟踪你的应用程序中是否有运行内存需要。

C#引用类型和值类型在堆、栈中的存储

Line 2:现在将会执行第二步。栈会将此处的一小块内存分配叠加在刚刚第一步的内存分配的顶部。你可以认为栈就是一个一个叠加起来的房间或盒子。在栈中,数据的分配和解除都会通过LIFO (Last In First Out)即先进后出的逻辑规则进行。换句话说,也就是最先进入栈中的数据项有可能最后才会出栈。

C#引用类型和值类型在堆、栈中的存储

Line 3:在第三行中,我们创建了一个对象。当这一行被执行后,.NET会在栈中创建一个指针,而实际的对象将会存储到一个叫做“堆”的内存区域中。“堆”不会监测运行内存,它只是能够被随时访问到的一堆对象而已。不同于栈,堆用于动态内存的分配。

需要注意的重点是对象的引用指针是分配在栈上的。 例如:声明语句 Class1 cls1; 其实并没有为Class1的实例分配内存,它只是在栈上为变量cls1创建了一个引用指针(并且将其默认值为null)。只有当其遇到new关键字时,它才会在堆上为对象分配内存。

C#引用类型和值类型在堆、栈中的存储

离开这个Method1方法时:现在执行的控制语句开始离开方法体,这时所有在栈上为变量所分配的内存空间都会被清除。换句话说,在上面的示例中所有与int类型相关的变量将会按照“LIFO”后进先出的方式从栈中一个一个地出栈。

注意:这时它并不会释放堆中的内存块,堆中的内存块将会由垃圾回收器稍候进行清理。

C#引用类型和值类型在堆、栈中的存储

四、总结

Heap space 堆空间: 所有存活的对象在此分配.

Stack space 栈空间: 方法调用时保存变量对象的引用或变量实例.

在C#中只要是成员变量,一旦它所在类被实例化后,都是作为一个整体放在堆内存的,不管它是值类型还是引用类型。局部变量才是放在栈内存的。而类的方法是所有的对象共享的,方法是存在方法区的,只用当调用的时候才会被压栈,不用的时候是占内存的。

简单来说,值类型和引用类型变量本身在栈中分配内存,引用类型的实例在堆中分配内存。(要注意的是,一定要理解清楚引用类型变量本身和引用类型的实例的区别,引用类型变量好比一个指针,它所指向的内容即引用类型的实例)。有时候又会看到一些说法,值类型在其所定义的位置分配内存,这让人感到很混乱却也不得不在意,下面简单捋一下。

如下面代码:

public class TestClass 
{
      int a = 0;
}
 
public void Function()
{
     int b = 0;
     TestClass class1 = new TestClass();
}
           

方法Function中定义的值类型(int b)在栈中分配,引用类型(类class1)以一个类似于指针的形式也存储于栈中,而类的实例对象即代码中class1所引用的实际数据(整型a)是在堆上面分配的。这就可以理解,为什么有的地方会说“值类型在其所定义的地方分配”,因为上述代码中的值类型a由于定义在类中,作为类的一个成员,在类实例化时是被分配到堆中的。

因此,更进一步地,可以理解为:值类型作为一个方法中的局部变量时,是在栈中分配的,而当作为类的成员变量时,是分配在堆中的。

在上面代码的TestClass中添加一个Run函数:

public class TestClass 
{
    int a = 0;
    public void Run()
    {
        int c = 0;
    }
}
 
public void Function()
{
    int b = 0;
    TestClass class1 = new TestClass();
    class1.Run();
}
           

此时,整型a是在堆中分配内存,而Run函数中的整型c在栈中分配内存。

因此关于值类型、引用类型各自在堆或栈上的内存分配可以总结为:

值类型作为方法中的局部变量时,在栈中分配,而作为类的成员变量时,在堆中分配;引用类型变量在栈中分配,引用类型的实例在堆中分配。