简介:
本文将介绍6个重要的概念,分别是堆栈,堆,值类型,引用类型,装箱和拆箱。首先会简单的解释一下当声明了一个变量的时候,程序的背后会发生什么;之后会介绍堆栈(Stack)和堆(Heap)的概念,然后围绕值类型(Value Type)和引用类型(Reference Type)进行一些探讨。
文章的最后一部分通过一个例子来说明装箱(Boxing)和拆箱(Unboxing)对程序性能方面的影响。
(图片引用自http://michaelbungartz.wordpress.com/)
声明变量时发生了什么?
当你在.NET程序中声明了一个变量的时候,它将会为你在内存中申请一段存储空间,这部分存储包括3个部分: 1.变量名称 2. 变量的类型 3.变量的值
对于不同数据类型,.NET中的变量可能会被分配在堆栈或堆之上,下面我们将会详细的讨论这两种不同的存储类型。
堆栈和堆
我们来分析下面的代码:
{
// Line 1
int i = 4 ;
// Line 2
int y = 2 ;
// Line 3
class1 cls1 = new class1();
}
Line1: 当执行这一段代码的时候,编译器将会在被称作"堆栈"的存储空间中分配一部分内存用于存储变量i。栈同时将会负责监控这部分内存的使用情况。
Line2:程序继续执行到这部分代码。正如"栈"的名称所表示的那样,程序将在刚才分配的空间的“栈顶”上再分配一部分内存用于存储变量y。
可以假想“堆栈”就是许多的的箱子,这些箱子一个接一个的叠放成一摞。
堆栈上存储空间的分配和释放遵循先进后出(LIFO)的原则,也就是说存储空间的分配和释放操作只能在堆栈的一端进行。(其实就是只能在栈顶进行)
Line3:我们在第三行创建了一个对象(Object)。当执行到这段代码的时候,编译器将在堆栈上创建一个指针,而实际的对象将被存储在被称为“堆”的存储空间。“堆”不像“堆栈”那样监控内存的使用情况,它仅仅是将其中的对象排列起来,这部分内存在任何情况下都可以被程序访问。堆用于动态的内存分配。
这里需要指出的一点是:cls1是被分配在堆栈上的。如果仅仅是声明一个对象,例如
{
对于Class1 cls1; 堆上并没有创建任何的存储空间用于存储Class1的对象,它仅在堆栈上创建以一个变量cls1(指向为空)。当执行到new关键字的时候,程序才会在堆上分配相应的内存。
退出方法时:当程序执行到方法的结尾时,将会释放掉该方法在堆栈上分配的空间。换句话说,int类型的变量将按照先进后出的原则被释放出堆栈空间。
这时堆上分配的内存并没有得到释放,这部分内存将会在之后被垃圾收集器收集并最终释放。
有人可能会有疑问,为什么要有两种存储空间类型,难道不能将所有的变量都分配到同一类型中去么?
仔细研究后你会发现,原生数据类型(primitive data type)往往比较简单,他们所包含的数据也很单一,比如“int i = 0”。 而对象数据类型(object data type)往往比较复杂,在对象数据类型中可能会包含其他的对象数据类型和原生数据类型。也就是说对象数据类型中含有多个“内容”而且这些“内容”都必须被存储在内存中。所以对象数据类型需要动态存储空间,而原生数据类型需要静态存储空间。动态存储空间将被分配在堆上而静态存储空间则分配在堆栈上。
值类型和引用类型
当简单了解了堆栈和栈之后,我们来看看值类型和引用类型的概念。
值类型将其所包含的值和其所在的地址存储在一起,而引用类型只包含一个指向其存储地址的指针。
下面的例子中我们将整形变量i的值赋给另一个整形变量j,i和j都的值将被分配到堆栈上。
当我们将一个int变量值赋给另一个int变量时,将会创建一个完全不同的拷贝。也就是说当其中一个值发生变化后,另一个值并不会受到影响。这种数据类型被称作是“值类型”。
当我们创建了一个对象并将该对象赋值给其他对象时,两者都将指向同一内存地址。如下图所示,obj和obj1指向堆上的同一地址。
在这种情况下,当其中一个对象的值被修改之后,另一个对象也会受到影响。我们称这种类型为“引用类型”。
我们有哪些值类型和引用类型?
在.NET中,变量被分配在堆栈上或是分配在堆上是由变量的类型决定的。 “String”和“Objects”是引用类型,而其他的原生类型则将被分配在堆栈上(值类型)。如图所示:
装箱与拆箱
好了,我们已经了解了这么多的相关的知识,那么它们在实际的编程当中有什么用呢?我们可以用它们来帮助理解数据在堆栈和堆之间的转移所带来的性能方面的影响。
来看看下图中的例子。当我们将值类型转换为引用类型时,数据从堆栈上转移到堆上。反过来,将引用类型转换为值类型时,数据从堆转移至堆栈。这种从堆和堆栈的数据转移,将对程序的性能产生不利的影响。
值类型转换为引用类型,我们称之为“装箱”(Boxing),反之,称之为“拆箱”(Unboxing)。
用ILDASM 反编译上面的代码,可以通过中间代码(IL)了解到装箱和拆箱操作,如下图所示:
装箱和拆箱的对程序造成的性能影响
我们可以分别执行下列的两个方法各10000次,第一个方法包含装箱的操作,而另一个比较简单。
含有装箱操作的方法需要执行3542毫秒,而另一方法执行了2477毫秒,所以,在实际应用当中,应该尽量避免出现装箱和拆箱的操作,仅在必需的情况下,再去使用它们。