日期:2013-06-22  浏览次数:20437 次

由一个性能问题引出的.net概念
关键字:.net 性能 GC 值类型 引用类型 堆 堆栈 string

1 引子
我们先来看一下两组代码,每组中的哪一段代码效率更高呢?

第一组:

代码1:

for(int i = 0; i < 10000; i++)

{

AddressData ds = new AddresssData();

ds = addressS.GetAddress();

}



代码2:

for(int i = 0; i < 10000; i++)

{

AddressData ds;

ds = addressS.GetAddress();

}

第二组:

代码一:

string strNames = "@"+Guid.NewGuid().ToString().Replace("-","")+ ",@"+Guid.NewGuid().ToString().Replace("-","")+ ",@"+Guid.NewGuid().ToString().Replace("-","");

for(int i = 0; i < 10000; i++)

{

……

}



代码2:

for(int i = 0; i < 10000; i++)

{

string strNames = "@"+Guid.NewGuid().ToString().Replace("-","")+ ",@"+Guid.NewGuid().ToString().Replace("-","")+ ",@"+Guid.NewGuid().ToString().Replace("-","");

……

}

每一组代码中,两段代码实现的功能是一样的,它们之间的区别也很小,但是其效率会相差得惊人,其中一种会很频繁的GC,为什么呢?

在回答这个问题的时候我们先了解几个.net概念。

2 什么是GC
GC的全称是garbage collection,中文名称垃圾回收,是.net中对内存管理的一种功能。垃圾回收器跟踪并回收托管内存中分配的对象,定期执行垃圾回收以回收分配给没有有效引用的对象的内存。当使用可用内存不能满足内存请求时,GC会自动进行。

在进行垃圾回收时,垃圾回收器回首先搜索内存中的托管对象,然后从托管代码中搜索被引用的对象并标记为有效,接着释放没有被标记为有效的对象并收回内存,最后整理内存将有效对象挪动到一起。这就是GC的四个步骤。

由上可见,GC是很影响性能的,所以一般说来这种事情况还是尽量少发生为好。

为了减少一些性能影响,.net的GC支持对象老化,或者说分代的概念,代是对象在内存中相对存现时期的度量单位,对象的代数或存现时期说明对象所属的代。目前.net的垃圾回收器支持三代。每进行一次GC,没有被回收的对象就自动提升一代。较近创建的对象属于较新的代,比在应用程序生命周期中较早创建的对象的代数低。最近代中的对象位于零代中。每一次GC的时候,都首先回收零代中的对象,只有在较低代数的对象回收完成后仍不能满足需求的情况下才回收较高代数的对象。

3 堆栈和堆
内存有堆栈和堆的概念。堆栈遵循后进先出的原则,后被推入堆栈的对象必定实现本拉出堆栈,这样保证了这部分内存的紧凑,也基本上不需要考虑内存地址的问题。而堆则没有这个原则,任何一个对象都有可能在任何时候进入堆中,也可能在任何时候被移出堆。这样很明显,我们就要考虑每一个对象保存在哪里,所以就需要在堆栈中保存每一个对象保存在堆中的地址。同时在经过一段时间之后我们就会发现堆中产生了许多空隙,也就是碎片,为了提高系统性能,我们这个时候经常需要整理堆,以清除碎片。关于堆栈和堆,如下图所示:


4 GC和堆栈、堆
由前述堆栈和堆的概念可以看出,堆栈不存在垃圾收集的问题,只需要直接压栈即可,而堆,则面临着很复杂的垃圾回收的问题。GC完全是对堆进行操作的,而对堆中对象是否有效的判断则是通过遍历堆栈来实现的。这里涉及到一个引用计数的概念,引用计数是对堆中对象被引用次数的统计,当一个对象的引用计数为零了,那么这个对象就可以被回收了。在进行GC的时候,垃圾回收器遍历堆栈,当发现一个堆地址的时候,它就将堆中该地址上的对象的引用计数加1,然后销毁堆中所有引用计数为零的对象,回收内存并整理堆中的碎片。

5 值类型和引用类型
我们都知道,计算机中的数据类型分为值类型和引用类型两种。那么到底什么是值类型什么是引用类型呢?

大多数编程语言提供内置的数据类型(比如整数和浮点数),这些数据类型会在作为参数传递时被复制(即,它们通过值来传递)。在 .NET Framework 中,这些称为值类型。运行库支持两种值类型:内置值类型和用户定义的值类型。

引用类型则存储对值的内存地址的引用。引用类型可以是自描述类型、指针类型或接口类型。引用类型的类型可以由自描述类型的值来确定。自描述类型进一步细分成数组和类类型。类类型是用户定义的类、装箱的值类型和委托。

作为值类型的变量,每个都有自己的数据副本,因此对一个变量的操作不会影响其他变量。作为引用类型的变量可以引用同一对象;因此对一个变量的操作会影响另一个变量所引用的同一对象。

6 值类型、引用类型和堆栈、堆
了解了值类型和引用类型,那么,这两种类型在内存中又是怎样表现的呢?

值类型存储在堆栈中,而引用类型则存储在堆中,然后在堆栈中存储对堆中对象的引用(又叫指针),如下图所示:


因为这样的一种存储方式,于是就造成了对变量操作影响的不同,例如通过引用指针b对数据所作的更改也会表现在通过引用指针c的得到的数据中。而对值类型进行操作却不会有这样的情况。

这两种不同也会表现在我们的方式上,例如:

我们假设ModifyClass()方法是对ClassA中的字段Value1加2;

ClassA ca = new ClassA();

ca.Value1 = 2;

ModifyClass(ca);

int getValue = ca.Value1;

……

这时你可以看到getValue的值是4,而ModifyClass()方法并没有返回任何数据。

而对于值类型,我们就会发现这样做是不可以的,你必须要让方法有返回数据,如:

int ca = 2

int getValue = ModifyValue(ca);

值类型和引用类型的区别还在于声明新变量的时候,如ClassA ca = null是合法的,而int ca = null则是非法的。

7 类实例化的步骤
类是最常见也是我们用的最多的一种引用类型,我们知道实例化一个类使用的是一个我们司空见惯的语句:

ClassA ca = new ClassA();

那么这短短的一句话中,计算机又做了些什么事情呢?

实际上,计算机在这个过程中大致做了这么几件事:

首先,在ClassA ca的时候,生成一个空的引用指针,并将它推入堆栈中:


然后,在new ClassA()的时候,生成ClassA的新的实例,并放入堆中:


在赋值号=这一步,将ca的引用指针指向刚刚生成的新实例:


这个时候,才算完成了整条语句的操作。

好了,了解了以上这些概念之后,我们可以来回答本文开始的问题了:

8 回答本文开始的问题
关于第一组代码,我们首先要了解

AddressData ds = new AddresssData();