如何管好.net的内存(托管堆和垃圾回收)

时间:2023-03-09 21:37:33
如何管好.net的内存(托管堆和垃圾回收)

一:C#标准Dispose模式的实现

需要明确一下C#程序(或者说.NET)中的资源。简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类:

托管资源:由CLR管理分配和释放的资源,即由CLR里new出来的对象;

非托管资源:不受CLR管理的对象,windows内核对象,如文件、数据库连接、套接字、COM对象等;

毫无例外地,如果我们的类型使用到了非托管资源,或者需要显式释放的托管资源,那么,就需要让类型继承接口IDisposable。这相当于是告诉调用者,该类型是需要显式释放资源的,你需要调用我的Dispose方法。

不过,这一切并不这么简单,一个标准的继承了IDisposable接口的类型应该像下面这样去实现。这种实现我们称之为Dispose模式:

publicclass SampleClass : IDisposable
{
//演示创建一个非托管资源
private IntPtr nativeResource = Marshal.AllocHGlobal(100);
//演示创建一个托管资源
private AnotherResource managedResource =new AnotherResource();
privatebool disposed =false; ///<summary>
/// 实现IDisposable中的Dispose方法
///</summary>
publicvoid Dispose()
{
//必须为true
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)
GC.SuppressFinalize(this);
} ///<summary>
/// 不是必要的,提供一个Close方法仅仅是为了更符合其他语言(如C++)的规范
///</summary>
publicvoid Close()
{
Dispose();
} ///<summary>
/// 必须,以备程序员忘记了显式调用Dispose方法
///</summary>
~SampleClass()
{
//必须为false
Dispose(false);
} ///<summary>
/// 非密封类修饰用protected virtual
/// 密封类修饰用private
///</summary>
///<param name="disposing"></param>
protectedvirtualvoid Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
// 清理托管资源
if (managedResource !=null)
{
managedResource.Dispose();
managedResource =null;
}
}
// 清理非托管资源
if (nativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(nativeResource);
nativeResource = IntPtr.Zero;
}
//让类型知道自己已经被释放
disposed =true;
} publicvoid SamplePublicMethod()
{
if (disposed)
{
thrownew ObjectDisposedException("SampleClass", "SampleClass is disposed");
}
//省略
}
}

 

在Dispose模式中,几乎每一行都有特殊的含义。

在标准的Dispose模式中,我们注意到一个以~开头的方法:

///<summary>
/// 必须,以备程序员忘记了显式调用Dispose方法
///</summary>
~SampleClass()
{
//必须为false
Dispose(false);
}

这个方法叫做类型的终结器。提供终结器的全部意义在于:我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终结器会被垃圾回收器调用这个特点,终结器被用做资源释放的补救措施。

一个类型的Dispose方法应该允许被多次调用而不抛异常。鉴于这个原因,类型内部维护了一个私有的布尔型变量disposed:

        private bool disposed = false;

在实际处理代码清理的方法中,加入了如下的判断语句:

if (disposed)
{
return;
}
//省略清理部分的代码,并在方法的最后为disposed赋值为true
disposed =true;

这意味着类型如果被清理过一次,则清理工作将不再进行。

应该注意到:在标准的Dispose模式中,真正实现IDisposable接口的Dispose方法,并没有实际的清理工作,它实际调用的是下面这个带布尔参数的受保护的虚方法:

///<summary>
/// 非密封类修饰用protected virtual
/// 密封类修饰用private
///</summary>
///<param name="disposing"></param>
protectedvirtualvoid Dispose(bool disposing)
{
//省略代码
}

之所以提供这样一个受保护的虚方法,是为了考虑到这个类型会被其他类继承的情况。如果类型存在一个子类,子类也许会实现自己的Dispose模式。受保护的虚方法用来提醒子类必须在实现自己的清理方法的时候注意到父类的清理工作,即子类需要在自己的释放方法中调用base.Dispose方法。

还有,我们应该已经注意到了真正撰写资源释放代码的那个虚方法是带有一个布尔参数的。之所以提供这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源。

在供调用者调用的显式释放资源的无参Dispose方法中,调用参数是true:

publicvoid Dispose()
{
//必须为true
Dispose(true);
//其他省略
}

这表明,这个时候代码要同时处理托管资源和非托管资源。

在供垃圾回收器调用的隐式清理资源的终结器中,调用参数是false:

~SampleClass()
{
//必须为false
Dispose(false);
}

这表明,隐式清理时,只要处理非托管资源就可以了。

那么,为什么要区别对待托管资源和非托管资源。在认真阐述这个问题之前,我们需要首先弄明白:托管资源需要手动清理吗?不妨先将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,我们暂时称之为非普通类型,后者我们称之为普通类型。非普通类型因为包含非托管资源,所以它需要继承IDisposable接口,但是,这个包含非托管资源的类型本身,它是一个托管资源。所以说,托管资源需要手动清理吗?这个问题的答案是:托管资源中的普通类型,不需要手动清理,而非普通类型,是需要手动清理的(即调用Dispose方法)。

Dispose模式设计的思路基于:如果调用者显式调用了Dispose方法,那么类型就该按部就班为自己的所以资源全部释放掉。如果调用者忘记调用Dispose方法,那么类型就假定自己的所有托管资源(哪怕是那些上段中阐述的非普通类型)全部交给垃圾回收器去回收,而不进行手工清理。理解了这一点,我们就理解了为什么Dispose方法中,虚方法传入的参数是true,而终结器中,虚方法传入的参数是false。

二:如何管好.net的内存

.net的性能瓶颈,毫无疑问是在内存管理上面。自动内存回收解决不了所有的问题,反而会制造性能问题。所以大批c++专家都不赞同在c++内部添加类似.net的内存管理机制,只是有保留的通过程序库来支持相关技术。

java老爱说c/c++管不好内存,容易泄露。但是其实本质上还不是将本来该由终端程序员自己处理的事情,交给了框架开发人员来处理了。

既然都是程序员,凭什么说你这些框架开发者就不会出现人为错误?

他们是专家,对大部分菜鸟级别的程序员来说,这个策略有点帮助,这是当然。但是从根本上,交给框架开发人员处理是有局限性的。他们无法预料到最终的应用程序有怎样的需求,脱离了具体的开发环境,也就无法做出最体贴的解决方案出来。

因此,也不要认为.net之类的内存管理必然就很先进,有这么大的局限性存在,还敢号称什么“无泄漏”?

内存泄漏是怎样的概念?我想说说我的看法。

我认为内存如果不在它该释放的那一刻,到需要再度利用这段时间,做到平滑,就不能算是优秀的内存管理。

.net的管理机制是惰性的,非到迫不得已,都不会释放掉,结果就把内存无端端的占用掉了,系统可用内存无端端就少了一大截。

这种和泄漏也差不了多少,某种程度上甚至还更糟糕。比如你泄漏一整天,就几兆,无伤大雅,但是你却无端端占用几百兆的内存,这个对系统的性能影响更大。所谓的泄漏不泄漏,不是本质,本质是对系统的负担有多大,内存管理机制有多高效。

有没有办法,让我们对.net的内存管理更加主动?这就是这篇文章要探讨的内容。

在开发过程中,性能问题往往出现在滥用框架功能上面。比如string不适合拼接字符串,程序员却滥用了。诸如此类的,解决方案就是我们有能力去理解框架提供的内容和重新设计一个更加有效率的编码。为什么说我们不能忘掉算法这些,原因就在此,框架的功能并不能适用在特定需求上面的,如果离开框架我们就无法工作,那样做出来的产品往往就只是二三流水准。这些内容不需要很多,却又必不可少。

这就是控制力的问题,如何做到更加细致的控制,是关键时刻解决问题的制胜之道。在直路,赛车手和平民的区别也许相对来说不是很大,但是拐弯就体现真功夫。我们可能要花90%的力气去做这10%的工作,才能保证软件的品质。

如何提高我们对.net托管内存的控制?

一、理解.net的内存管理机制。

二、了解框架要求程序员负责的任务。

三、了解手动控制的手段。

以c#语言为例。

析构函数如: ~类名() 默认是不自动生成的,也不能继承。但是析构函数并不管理内存,.net把内存作为资源的独立部分来管理,内存自动管理,其余资源依赖程序员自己通过析构函数等手段来管理。

因为内存是自动管理的,程序员只需要了解第三点:手动控制的手段。

System.GC.Collect() 强制启动内存垃圾回收程序。

第二个问题是除内存之外的其余资源如何管理。

.net 把任何实现了析构函数的对象,加入一个待清理队列中,这个队列会去调用析构函数,这个时候就能释放资源了。

因此,只要实现了析构函数,然后把释放资源的代码放在析构函数内部,就能交由.net处理余下事项,你也不需要担心自己忘了清理。

如果我们想要他在立即执行这个操作,可以调用System.GC.Collect()来启动。

问题是,.net实现这个机制的代价是很高的,如果我们又要释放资源,又不想影响效率,而且又担心我们会忘了释放资源,可以采取以下设计模式:

1.实现一个释放资源的函数。方便我们手工调用。

2.在析构函数中调用这个函数。

3.释放资源的函数中调用System.GC.SupressFinalize(),就会把对象从.net的待清理队列中释放出来,这样.net就不会启动释放机制了。

4.必须有一个标志来判断是否已经把资源释放了,避免重复释放资源。

5.必须有一个标志来判断是否需要调用System.GC.SupressFinalize()。如果程序员忘了手动清理,而是.net自身通过调用析构函数去清理的,那把对象从待清理队列中释放已经是没有意义的事情了。

可见,如果要高效,还是免不了要程序员自己记得手工释放。

以上是管理内存和其余资源的方式,至于如何运用,我个人的看法是,只要资源没有利用价值了,就应该尽快释放掉,而不是等缓慢的.net回收机制。

本文文章转载自:

http://www.cnblogs.com/Nobel/archive/2010/12/19/1910626.html(改善C#程序的建议4:C#中标准Dispose模式的实现)

http://www.cnblogs.com/luminji/archive/2011/03/29/1997812.html(如何管好.net的内存)