欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  后端开发

C# 值类型和引用类型的内存管理

程序员文章站 2022-05-22 18:45:17
...
本次日志,我们来重点聊一聊软件开发过程中,如何提高性能方面的问题。这是软件开发或研发过程中深层次的问题,这篇文章主要从内存分配和内存回收两方面说明,我们软件代码编写过程中,计算如何来工作的。在此你可以了解内存管理的过程和方式,以便在以后的软件开发中注意它、利用它。

值类型包括: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()方法了。