首页 > 技术文章 > .NET类型与堆栈浅析

xuejietong 2019-04-06 12:06 原文

引用类型与值类型在语义上的区别

  .NET中的引用类型包括类、委托、接口、数组,字符串(String),值类型包括结构和枚举以及基元类型(int、float、decimal等)。引用类型具有引用语义,考虑的是对象的标识,而非其内容;值类型具有值语义,对象没有标识,访问对象时是直接访问其内容而非引用。

值类型和引用类型在语义上的区别
条件 引用类型 值类型
将对象传递给方法

传递引用,

对对象的改变会影响到其它引用

传递内容(ref、out除外),

对对象的改变不会影响到外部代码

将变量赋值给变量

赋值引用,

两个变量的引用将指向相同的对象

赋值内容,

两个对象的内容将完全相同,但也毫不相干

使用operator==进行比较

比较引用,

若指向相同对象则相等

比较内容,

若对象每个字段相同则相等

 

 

存储、分配、销毁

  引用类型只从托管堆上分配,会涉及指针递增,指针的操作对性能来说比较高效。在多处理器系统中,如果多个处理器访问堆上相同的对象时将需要进行同步,但相对非托管环境(如使用malloc)进行分配来说,这仍然是非常低廉的操作。垃圾回收器以一种非确定性的方式进行内存回收,并且无法保证进行了哪些内部操作。一次完整的垃圾回收代价是非常高的,但一个构造良好的应用程序,其平均垃圾回收成本应该比相应的非托管应用小的多。

  单纯的值类型通常在执行线程的栈上分配,但值类型可以内嵌于引用类型,这时值类型就被分配在堆上。值类型也可以被装箱,将存储转移到堆上。从栈上分配值类型是一个非常低廉的操作。包括修改栈指针寄存器,并且在同时分配多个对象时还有额外的优势。回收栈上的内存也非常高效,只需要反向修改栈指针寄存器。

  严格来说,某些引用类型也可以从栈上分配。例如,使用unsafe上下文和stackalloc关键字创建的基元类型的数组(如整型数组),或者使用fixed关键字在自定义结构中内嵌一个固定大小的数组,都可以实现在栈上分配引用类型。不过,通过stackalloc和fixed关键字创建的对象并不是真正的数组,它们的内存布局(memory layout)与在堆上分配的标准数组是有差别的。

  在C#和其它托管语言中,new关键字并非只能用于堆分配,也可以使用new关键字在栈上分配值类型。例如,DateTime dt=new DateTime(2019,04,06)。

 

栈和堆的不同

  在.NET进程中,栈和堆的区别并不大。栈和堆都是虚拟内存上的地址范围,线程栈所保留的地址范围与托管堆所保留的地址范围相比,并没有先天优势;访问栈上的内存地址和访问堆上的内存地址相比,并没有快或慢的不同。

  在栈上,时间分配的局部性(分配时间很接近)意味着空间局部性(存储地址很接近)。同样,时间分配的局部性意味着时间访问的局部性(一起分配的对象一起被访问)。连续的栈存储能充分利用CPU缓存和操作系统的分页系统,往往具有更好的性能。

  栈上的内存密度通常比堆上要高,这是因为引用类型需要额外的开销。更高的内存密度通常意味着更高的性能。例如,CPU可以更多的缓存对象。

  线程栈往往都很小。Windows上默认情况下栈最大1M,并且大多数线程通常只使用很少的栈页。应用程序的线程栈可用于CPU缓存,这使得栈上对象的访问速度非常之快。相比之下,堆上的对象很少能用于CPU缓存。但不能将所有分配都放在栈上,Windows的线程栈资源是有限的,很容易被错误的递归和大型栈分配耗尽。

推荐阅读