声明
前言
本文将会讲解有关 .NET 的一些重要的内部机制。
- 当你声明一个变量时,涉及到的
栈 和堆 的概念 - 值类型与引用类型
- 装箱和拆箱机制的性能影响
声明一个变量的背后发生了什么
当你在一个.NET应用中声明一个变量,它会分配一些内存块。这块空间会包含三个东西:
以上的解释简化了很多,实际上,数据类型的不同将会导致其被分配到对应种类的内存空间。内存的分配类型有两种:
栈和堆
为了理解栈和堆,我们先理解以下代码的内部机制:
1 | public void Method1() |
让我们来分析这三行代码做了什么事情。
内存的分配与释放遵循



还有一个重要的点是,对象的引用会被分配到栈上。语句Class1 cls1;不会给Class1的实例分配内存,而只是在堆上分配一个变量叫cls1(并将其设置为null)。当你使用new关键字时,才会在堆上分配内存。
在程序运行的最后,栈上的内存会被释放。换句话说,所有类似int数据类型的变量都会以LIFO的顺序被释放。
然而对于堆来说,程序的退出不会
此时可能会有人问:为什么要区分出两种不同的内存分配方式呢?为什么不直接将所有的内存都分配到其中一种上呢?
你会发现,.NET的原始数据类型并不复杂且只被赋予了一个值,比如int i = 0。而对象的数据类型都很复杂,因为它们包含了对其他对象和原始数据类型的引用。
换句话说,他们有对多个值的引用,而每个值都必须要被存储到内存。引用类型需要动态内存,而原始数据类型只需要静态内存。如果需要动态内存,它就会被分配到堆上,否则就会被分配到栈上。
值类型和引用类型
现在我们理解了栈和堆的概念,我们来看看值类型和引用类型。
值类型在栈上存储了数据和内存地址,而引用类型只在栈上存储了内存地址。
一个简单的整型变量i,它被赋值为另一个整型变量y,那么i和y都会被分配到栈上。
当我们将一个整型变量赋值到另一个整型变量上,它们完全不同,仅仅是拷贝了值。换句话说,如果你改了其中一个变量的值,另一个并不会改变。这就是
1 | public void Method1() |


当我们创建了一个对象并让另一个对象引用该对象时,它们指向了同一块内存地址(如下图所示)。所以当我们将obj指派为obj1后,它们指向的内存地址相同。
也就是说,当我们改变其中一个对象的数据,其他引用了该对象的对象也会跟着改变。这就是
1 | public void Method1() |


词汇解释
- 类 (Class): 一种引用类型,是对数据和行为的抽象 (字段,属性和方法等),可以视作蓝图。
- 对象/实例 (Object): 是类 (Class) 实例化后的产物,可以视作是按照蓝图配置的内存块。
- 类的
实例化 是对象,对象的抽象 是类。 - Assign 一词在值类型的语境下可以理解为
赋值 ,在引用类型的语境下理解为指派/引用 ,在内存相关操作时理解为分配 。
哪些类型是值/引用类型
在.NET中,可以根据一个数据类型是分配在栈还是堆来分辨是值类型还是引用类型。比如String和Object是引用类型,其他的
常见类型
- 值类型:byte,short,int,long,float,double,decimal,char,bool 和 struct
- 引用类型:string, 由类声明的类型
装箱和拆箱
了解这些在实际编程中有什么用呢 —— 在于了解数据在堆和栈之间的移动带来的性能损失。
请看下面的代码片段。
- 当我们将值类型移动到引用类型时,数据会从栈移动到堆,这个过程称为
装箱 。 - 当我们将引用类型移动到值类型时,数据会从堆移动到栈,这个过程称为
拆箱 。
1 | public void Method1() |



装箱和拆箱在 IL 代码中表示如下:
1 | object O = i; |
1 | int j = (int)O; |
装箱和拆箱的性能
为了了解装箱和拆箱带来的性能影响,我们将以下两个方法执行10000次。第一个方法是简单的装箱操作,而另一个是拆箱操作。通过使用Stopwatch类进行简单的测试。
过时的测试
原文在2010年进行的测试如今来看没有参考价值,因为现代的 .NET 运行时已针对装箱和拆箱操作做了很多优化。
- 原文的装箱测试:10000次耗时3542毫秒
- 原文的拆箱测试:10000次耗时2477毫秒
1 | void Main() |
1 | void Main() |