目录
介绍
本文将解释六个重要概念:栈、堆、值类型、引用类型、装箱和拆箱。本文开始解释当您声明一个变量时内部会发生什么,然后继续解释两个重要的概念:栈和堆。然后,本文讨论了引用类型和值类型,并阐明了围绕它们的一些重要基础知识。
本文最后通过示例代码演示了装箱和拆箱对性能的影响。
图片取自http://michaelbungartz.wordpress.com/
当你声明一个变量时,里面有什么?
当您在.NET应用程序中声明一个变量时,它会在RAM中分配一些内存块。这个内存有三样东西:变量名、变量的数据类型和变量的值。
这是对内存中发生的事情的简单解释,但是根据数据类型,您的变量会分配到该类型的内存。内存分配有两种类型:栈内存和堆内存。在接下来的部分中,我们将尝试更详细地了解这两种类型的内存。
栈和堆
为了理解栈和堆,让我们了解下面代码内部实际发生的事情。
public void Method1()
{
// Line 1
int i=4;
// Line 2
int y=2;
//Line 3
class1 cls1 = new class1();
}
这是一个三行代码,让我们一行一行地了解内部是如何执行的。
内存分配和取消分配是使用LIFO(后进先出)逻辑完成的。换句话说,仅在内存的一端,即栈顶部分配和取消分配内存。
- 第 1行:执行此行时,编译器会在栈中分配少量内存。栈负责跟踪应用程序所需的运行内存。
- 第 2 行:现在执行移动到下一步。顾名思义,栈是将这个内存分配堆叠在第一个内存分配之上。您可以将栈视为一系列放在彼此顶部的隔间或盒子。
- 第 3行:在第3行中,我们创建了一个对象。执行此行时,它会在栈上创建一个指针,实际对象存储在称为“堆”的不同类型的内存位置中。“堆”不跟踪运行内存,它只是一堆可以随时访问的对象。堆用于动态内存分配。
这里要注意的更重要的一点是引用指针是在栈上分配的。该语句Class1 cls1;不为Class1的实例分配内存,它只分配一个栈变量cls1(并将其设置为null)。它运行到new关键字的时候,它在“堆”上分配。
退出方法(fun):现在终于执行控制开始退出方法。当它通过结束控制时,它清除所有分配在栈上的内存变量。换句话说,所有与int数据类型相关的变量都以“后进先出”方式从栈中解除分配。
大捕获——它没有解除分配堆内存。垃圾收集器稍后将取消分配此内存。
现在很多开发者朋友一定很疑惑,为什么会有两种类型的内存,难道我们不能把所有的东西都分配到一种内存类型上就完成了吗?
如果仔细观察,原始数据类型并不复杂,它们包含像“int i = 0”这样的单个值。对象数据类型很复杂,它们引用其他对象或其他原始数据类型。换句话说,它们持有对其他多个值的引用,并且每个值都必须存储在内存中。对象类型需要动态内存,而原始类型需要静态类型内存。如果需要动态内存,则将其分配在堆上,否则将进入栈。
图片取自http://michaelbungartz.wordpress.com/
值类型和引用类型
现在我们已经了解了栈和堆的概念,是时候了解值类型和引用类型的概念了。值类型是将数据和内存保存在同一位置的类型。引用类型有一个指向内存位置的指针。
下面是一个具有名称为i的简单整数数据类型,其值被分配给另一个具有名称j的整数数据类型。这两个内存值都分配在栈上。
当我们将int值分配给另一个int值时,它会创建一个完全不同的副本。换句话说,如果你改变其中一个,另一个不会改变。这些类型的数据类型称为“值类型”。
当我们创建一个对象并将一个对象分配给另一个对象时,它们都指向相同的内存位置,如下面的代码片段所示。所以当我们赋值obj给obj1时,它们都指向同一个内存位置。
换句话说,如果我们改变其中一个,另一个对象也会受到影响;这被称为“引用类型”。
那么哪些数据类型是引用类型,哪些是值类型?
在.NET中,根据数据类型,变量要么在栈上分配,要么在堆上分配。'String'和'Objects'是引用类型,任何其他.NET原始数据类型都在栈上分配。下图以更详细的方式解释了相同的内容。
装箱和拆箱
哇,你传授了这么多知识,那么在实际编程中又有什么用呢?最大的影响之一是了解由于数据从栈移动到堆(反之亦然)而导致的性能损失。
考虑下面的代码片段。当我们将值类型移动到引用类型时,数据从栈移动到堆。当我们将引用类型移动到值类型时,数据从堆移动到栈。
这种从堆到栈的数据移动,反之亦然,这会导致性能下降。
当数据从值类型移动到引用类型时,它被称为“装箱”,反之则被称为“拆箱”。
如果您编译上述代码并在ILDASM中看到相同的内容,您可以在IL代码中看到“装箱”和“拆箱”的样子。下图演示了相同的内容:
装箱和拆箱的性能含义
为了查看性能如何受到影响,我们运行了以下两个函数10,000次。一个函数有装箱,另一个函数很简单。我们使用秒表对象来监控所用的时间。
装箱函数在3542毫秒内执行,而没有装箱时,代码在2477毫秒内执行。换句话说,尽量避免装箱和拆箱。在需要装箱和拆箱的项目中,在绝对必要时使用它。
本文附带了示例代码,演示了这种性能影响。
目前,我还没有包括拆箱的源代码,但同样适用于它。您可以使用stopwatch类编写代码并对其进行试验。
关于源代码
随文章附有一个简单的代码,它演示了装箱如何产生性能影响。您可以在此处下载源代码。