C#值类型和引用类型的内存管理
本次日志,我们来重点聊一聊软件开发过程中,如何提高性能方面的问题。这是软件开发或研发过程中深层次的问题,这篇文章主要从内存分配和内存回收两方面说明,我们软件代码编写过程中,计算如何来工作的。在此你可以了解内存管理的过程和方式,以便在以后的软件开发中注意它、利用它。
值类型包括:int,float,double,bool,结构,引用,表示对象实例的变量
引用类型包括:类和数组;比较特殊的引用类型string、object
一般情况下:值类型存储在堆栈中(不包括包含在引用中的值类型,如类的值字段,类中的引用字段,数组的元素这些都是随引用存储在受管制的堆中);引用类型存储在受管制的堆中,为什么说是受管制的堆,下面会具体来谈。
几个概念:
虚拟内存:32位的计算机,每个进程拥有4G的虚拟内存。
受管制的堆(托管堆):受谁的管制?当然是无用单元收集器,即垃圾收集器。如何管理下面再说?
无用单元收集器:垃圾收集器除了会压缩托管堆、更新引用地址、还会维护托管堆的信息列表等等。
关于值类型的存储先看如下代码:
{
int age=20;
double salary=2000;
}
上面定义的两个变量,int age,告诉编译器需要给我分配4个字节的内存空间来存储age值,它是存储在堆栈上的。堆栈属入先进后出的数据结构,堆栈是从高位地址到低位地址存储数据的。计算机寄存器中保持着一个堆栈指针,他总是指向堆栈最底端的自由空间地址,当我们定义一个int类型的值时,堆栈指针递减四个字节的地址;当变量出了作用域后,堆栈指针相应的相应的递增四个字节的地址,它只是堆栈指针的上下移动,所以堆栈的性能是相当高的。
下面看一下引用类型的存储,还是先看代码:
{
Customer customerA;
customerA=new Customer(); //假定Customer实例占据32个字节
}
上面的代码第一行先声明了一个Customer引用,引用的名字为customerA,在堆栈上给此引用分配存储空间,存储空间的大小为4个字节,因为它只存储了一个引用,这个引用所指向的才是即将存储Customer实例的空间地址,注意此时customerA并没有指向具体的空间,它只是分配了一个空间而已。
第二行执行过程中,.net环境会搜索托管堆,寻找第一个未使用的、连续的32个字节空间分配给类的实例,并设置customerA指向这段空间的顶端位置(堆的空间是从低到高使用的)。当引用变量出作用域后,堆栈中的引用会无效,当托管堆中的实例还在,直到垃圾收集器对其进行清理。
到这里细心的读者可能会有些疑问,是不是定义引用类型时,计算机要搜索整个堆,寻找足够大的内存空间来存储对象呢?这样会不会效率很低?如果没有足够大的连续的空间呢?这个就要谈到“托管”了。堆是受垃圾收集器管理的,.net在执行垃圾收集器时释放能释放的所有对象,并压缩其他对象,然后把所有自由空间组合在一起移动到堆的顶端, 形成连续的块,同时更新其他移动对象的引用。如果再有对象定义,可以很快找到合适的空间。如此看来托管堆工作方式与堆栈类似,它是通过堆指针来完成空间的分配和回收的。
上面谈了.net对内存空间分配的管理过程和方式,接下来谈一谈对内存的回收过程。谈到资源的清理,不得不提到的两个概念和三个方法。
两个概念为:托管资源和非托管资源。
托管资源接受.net framework的CLR(通用语言运行时)的管理;非托管资源则不受它的管理。
三个方法为:Finalize(),Dispose(),Close()。
一、Finalize()为析构方法,清除非托管资源。
在类中定义方式:
public ClassName{
~ClassName()
{
//清理非托管资源(如关闭文件和数据库联接等)
}
}
它的特点是:
1. 运行不确定性。
它是受垃圾收集器的管理,当垃圾收集器工作时,会调用此方法。
2. 性能开销大。
垃圾收集器工作方式为,对象如果执行了Finalize()方法,垃圾收集器第一次执行时,会把它放在一个特殊的队列中;第二次执行的时候才会删除此对象。
3. 不能显示定义和调用,定义为析构方法形式。
基于以上特点,最好不要执行Finalize()方法,除非类确实需要它或与其他两个方法结合来用。
二、Dispose()方法,可清除一切需要清除的资源,包括托管和非托管资源。
定义如下:
public void Dispose()
{
//清理应该清理的资源(包括托管和非托管资源)
System.GC.SuppressFinalize(this); //这一句很重要,下面会解释原因。
}
它的特点:
1. 任何客户代码都应显示调用这个方法,来释放资源。
2. 由于第一点的原因,一般要做一个备份,这个备份一般由析构方法来担任角色。
3. 定义此方法的类,必须继承IDisposable接口。
4. 语法关键字using等同于调用Dispose(),使用using时,它是默认调用Dispose()方法。所以使用using的类也要继承IDisposable接口。
Dispose()方法比较灵活,在资源不需要时立即释放。它是资源的最终处理,调用它意味着会最终删除对象。
三、Close()方法,暂时处置资源的状态,可能以后还会使用。一般处理非托管资源。
定义如下:
public viod Close()
{
//对非托管资源状态的设置,如关闭文件或数据库连接
}
它的特点:
对非托管资源状态的设置,一般是关闭文件或数据库连接。
下面写一个综合且又经典的的例子,利用代码演示一下各部分的作用:(为了省事,这个例子是从网上杜撰来的,只要说明问题就可以了,你说呢。在此应该感谢代码原创者,感谢他写了这么经典和易懂的代码,我们受益匪浅!)
public class ResourceHolder : System.IDisposable{
public void Dispose()
{
Dispose(true);
System.GC.SuppressFinalize(this);
// 上面一行代码作用是防止"垃圾回收器"调用这个类中的析构方法
// " ~ResourceHolder() "
// 为什么要防止呢? 因为如果用户记得调用Dispose()方法,那么
// "垃圾回收器"就没有必要"多此一举"地再去释放一遍"非托管资源"了
// 如果用户不记得调用呢,就让"垃圾回收器"帮我们去"多此一举"吧 ^_^
// 你看不懂我上面说的不要紧,下面我还有更详细的解释呢!
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// 这里是清理"托管资源"的用户代码段。
}
// 这里是清理"非托管资源"的用户代码段。此处为析构方法的实际执行代码,为了避免客户代码忘记显示调用Dispose()方法,所作的备份。
}
~ResourceHolder()
{
Dispose(false); // 这里是清理"非托管资源"
}
}
如果看不明白以上代码,一定要仔细阅读以下解释,很经典,不看会后悔呦。
这里,我们必须要清楚,需要用户调用的是方法Dispose()而不是方法Dispose(bool),然而,这里真正执行释放工作的方法却并不是Dispose(),而是Dispose(bool) ! 为什么呢?仔细看代码,在Dispose()中,调用了Dispose(true),而参数为"true"时,作用是清理所有的托管资源和非托管资源;大家一定还记得我前面才说过,"使用析构方法是用来释放非托管资源的",那么这里既然Dispose()可以完成释放非托管资源的工作,还要析构方法干什么呢? 其实,析构方法的作用仅仅是一个"备份"!
为什么呢?
格地说,凡执行了接口"IDisposable"的类,那么只要程序员在代码中使用了这个类的对象实例,那么早晚得调用这个类的Dispose()方法,同时,如果类中含有对非托管资源的使用,那么也必须释放非托管资源! 可惜,如果释放非托管资源的代码放在析构方法中(上面的例子对应的是 " ~ResourceHolder() "),那么程序员想调用这段释放代码是不可能做到的(因为析构方法不能被用户调用,只能被系统,确切说是"垃圾回收器"调用),所以大家应该知道为什么上面例子中"清理非托管资源的用户代码段"是在Dispose(bool)中,而不是~ResourceHolder()中! 不过不幸的是,并不是所有的程序员都时刻小心地记得调用Dispose()方法,万一程序员忘记调用此方法,托管资源当然没问题,早晚会有"垃圾回收器"来回收(只不过会推迟一会儿),那么非托管资源呢?它可不受CLR的控制啊!难道它所占用的非托管资源就永远不能释放了吗? 当然不是!我们还有"析构方法"呢! 如果忘记调用Dispose(),那么"垃圾回收器"也会调用"析构方法"来释放非托管资源的!(多说一句废话,如果程序员记得调用Dispose()的话,那么代码"System.GC.SuppressFinalize(this);"则可以防止"垃圾回收器"调用析构方法,这样就不必多释放一次"非托管资源"了) 所以我们就不怕程序员忘记调用Dispose()方法了。
补充:软件开发 , C# ,