Skip to content

堆与栈

栈的特征

  • 空间较小,读取速度快,内存空间由系统自动自动分配和维护
  • 数据只能从顶端插入(入栈push)和删除(出栈pop)
  • 简单的说就是先进后出或后进先出

堆的特征

  • 空间较大,读取速度慢,内存空间由用户控制和释放或由系统gc回收
  • 堆里内存能够以任意顺序存入和移除
  • 为经过排序的树形数据结构,每个节点都有一个值

全局区(静态区)

  • 全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。,程序结束后由系统释放

文字常量区

  • 程序结束后由系统释放

程序代码区

  • 存放函数体的二进制代码

值类型与引用类型

值类型

  • 只需要一段单独内存,存储实际的数据
  • 类型 byte,short,int,long,float,double,decimal,char,bool,struct,enum,可空类型
  • 值类型声明后,不管是否已赋值,编译器都会为其分配内存
  • 值类型通常存储在栈上,某些情况下可以存储在堆中(什么情况呢?)。

引用类型

  • 需要两段内存,一段位于堆中,存储实际数据,一段位于栈中,指向数据在堆中的内存地址
  • 类型 string,class,object,接口,委托。(数组的元素不管是引用类型还是值类型,都存储在托管堆上)
  • 当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中
  • 引用类型的对象总是在堆中分配 string是特殊引用类型,类型是不可变的,每次赋值会开辟新的内存空间,替换引用,所以不会覆盖。

装箱和拆箱

装箱转换是指将一个值类型隐式地转换成一个object 类型,或者把这个值类型转换成一个被该值类型应用的接口类型interface-type。把一个值类型的值装箱,也就是创建一个object实例并将这个值复制给这个object。

和装箱转换正好相反,拆箱转换是指将一个对象类型显式地转换成一个值类型,或是将一个接口类型显式地转换成一个执行该接口的值类型。 拆箱的过程分为两步:首先,检查这个对象实例,看它是否为给定的值类型的装箱值。然后,把这个实例的值拷贝给值类型的变量。

尽量避免装箱

装箱和拆箱会造成相当大的性能损耗(相比之下,装箱要比拆箱性能损耗大),性能问题主要体现在执行速度和字段复制上。因此我们在编写代码时要尽量避免装箱和拆箱,常用的手段为:

  1. 使用重载方法
  2. 使用泛型。因为装箱和拆箱的性能问题,所以在.NET2.0中引用了泛型,他的主要目的就是避免值类型和引用类型之间的装箱和拆箱。我们常用的集合类都有泛型的版本,比如ArrayList对应着泛型的 List<T>,Hashtable对应着Dictionary<TKey, Tvalue>
  3. 如果在项目中一个值类型变量需要多次拆装箱,那么可以将这个变量提出来在前面显式装箱。
  4. ToString。表面上看值类型调用ToString方法是要进行装箱的,因为ToString是从基类 继承的方法。但是ToString方法是一个虚方法,值类型一般都重写了这个方法,所以调用ToString方法不会装箱。之前说过String.Format方法容易造成装箱,避免的最佳方法就是在调用这个方法前将所有的值类型参数都调用一次ToString方法。

对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。按三步进行:

  1. 首先从托管堆中为新生成的引用对象分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)。
  2. 然后将值类型的数据拷贝到刚刚分配的内存中。
  3. 返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。

可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。