浅谈CLR的内存分配和回收机制

时间:2022-01-29 04:49:25

浅谈CLR的内存分配和回收机制

相对于C++程序员来说,C#程序员是非常幸运的,至少我们不需要为内存泄漏(Memory Leak)而头疼,不需要负责内存的分配和回收。但这不意味着我们只需要知道new的语法就可以了,作为一个严肃的C#程序员,我们应该对此有所了解,有助于我们编写性能更好的代码。

主要内容:

CLR的内存分配机制

CLR的回收机制

 

一、CLR的内存分配机制

.NET Framework 的垃圾回收器管理应用程序的内存分配和释放。每次使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行库就会继续为新对象分配空间。

 
  
... object obj = new object (); ...

但是,内存不是无限大的。

 
  
public void FillMemory() { ArrayList memory = new ArrayList();
    // 输出填充前所占内存大小 Console.WriteLine( " used memory: " + GC.GetTotalMemory( false )); for ( int i = 0 ; i < 100000 ; i ++ ) { memory.Add( new object ( )); }
     
    // 输出填充后所占的内存大小 Console.WriteLine( " used memory: " + GC.GetTotalMemory( false )); }

最终,垃圾回收器必须执行回收以释放一些内存。垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间。当垃圾回收器执行回收时,它检查托管堆中不再被应用程序使用的对象并执行必要的操作来回收它们占用的内存。

二、CLR的内存回收机制

一般我们在程序中创建的对象大部分都是托管对象,可依靠GC自动进行内存的回收,但是对于封装了非托管资源的对象,就需要我们显式重载object.Finalize()接口来实现非托管资源的释放。

 
  
using System; using System.IO; public class Foo { private SomeComObject _com; public Foo() { _com = new SomeComObject() ; } // some other operation here... ~ Foo() { // release the unmanaged resource _com.Close(); } }

* 析构函数会在编译时会被翻译为protected void Finalize(),这是C#的析构函数的语法

GC在回收对象之前会调用Finalize()来实现非托管资源的释放,不过按照MSDN的说法,Finalize()会导致性能的降低。

“垃圾回收器使用名为“终止队列”的内部结构跟踪具有 Finalize 方法的对象。每次您的应用程序创建具有 Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。

实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用 Finalize 方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。” --[MSDN]

更加建议实现Sytem.IDisposable.Dispose()接口,用来实现对非托管资源的释放,这也是.Net Framework中常见的设计模式。那该怎么实现Dispose呢?

1、首先,Dispose接口应该释放自身对象所占用的资源,还应该调用基类的Dispose方法,释放基类部分所占用的资源。

 
  
public void Dispose() { // do something to release my unmanaged resource ReleaseMyResource(); base .Dispose(); }

2、前面说过Finalize()会导致性能问题,那么在执行Dispose以后就应该告诉GC不用在调用Finalize()了

 
  
public void Dispose() { // do something to release my unmanaged resource ReleaseMyResource(); base .Dispose(); // tell gc not to call Finalize() GC.SuppressFinalize( this ); }

当然我们完全可以定义一个MySpace.IClosable.Close(),通过实现这个接口来进行非托管资源的释放,不过这实在没有必要。

posted on 2006-03-03 14:00 Aero 阅读(1659) 评论(25)  编辑 收藏 引用 收藏至365Key 所属分类: DotNet

浅谈CLR的内存分配和回收机制

评论

 

FileStream什么时候变成非托管对象了?
我觉得在对象图上层的对象释放自身关联的子对象使用在Dispose模型配合终结器完成对象的释放。
本身GC就有一个周期,你可以试试弱引用。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-03 16:00

不错,一直对于内存分配不太重视
前几天正好遇到了一个用户控件特别耗内存的问题,回去看一下,呵呵~~~
     

 re: 浅谈CLR的内存分配和回收机制 2006-03-03 16:03

@A.Z
谢谢扶正,已经改过来了。

“使用在Dispose模型配合终结器完成对象的释放”是说
protected void Finalize()
{
Dispose();
}

?但既然已经执行了Dispose为什么还要再用GC回收?

GC的周期偶不太清楚,好像是指对象的存活时间的长短,能说的具体点吗?谢谢     

 re: 浅谈CLR的内存分配和回收机制 2006-03-03 18:38

public void Dispose()
{
// do something to release my unmanaged resource
ReleaseMyResource();
base.Dispose();

// tell gc not to call Finalize()
GC.SuppressFinalize(this);
}
楼主喜欢这样用呀,我的习惯和楼主不同,我喜欢这样用:
public void Dispose()
{
// tell gc not to call Finalize()
GC.SuppressFinalize(this);

// do something to release my unmanaged resource
ReleaseMyResource();
base.Dispose();

}

Sheva     

 re: 浅谈CLR的内存分配和回收机制 2006-03-03 21:49

自定义析构函数确实会对性能造成比较大的影响,前段时间就做了个测试,生成一个很大的数据集,里面包括很多数据类,在数据类定义了析构函数,内存就下来了,而且CPU也是100%,去掉析构函数马上就下来了。
《.NET 框架程序设计》有关于对象释放,内存释放的详细介绍。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-03 23:35

收获颇多
但是也剔一下骨头:
memory.Add(new string("foo"));??
这样写是错误地
呵呵
建议更正为memory.Add(new string[i]);
     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 00:03

@阿不

谢谢,有机会好好看看。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 00:05

@雁儿飞飞

new string("foo")的确犯了一个低级错误,已经改过来了,谢谢。
不过new string[i]??????     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 10:54

@Aero
"每次使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存"
new string[i]就是上句的实现
当然要想new的方法非常多
正如您的new object()

但是强调一点:
new object()在i<100下是看不出new前后内存的差别的
但是new string[i]在i<100就已经非常明显的看出new前后内存的差别


最后您的i<100000,虽然明显,但是对于那些内存小的机子,会导致人家死机地干活.
嘿嘿
共同交流     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 12:07

题目太大了,所以就有一点文不对题。

对Finalize的解释并对MSDN的引用在《最大化.Net性能》一书中讲得非常详细。不过你对Dispose方法的调用实在不敢恭维。一般是这样使用的:

~Foo()
{
Dispose(false);
}

void IDisposable.Dispose()
{
Dispose(true);
}

protected virtual void ReleaseUnmanageResources()
{
....
}

private void Dispose(bool disposing)
{
ReleaseUnmanageResources();
if (disposing)
{
GC.SuppressFinalize(this);
}
}
这样在执行Dispose时,释放托管资源并且通知GC不要调用Finalize方法;如果IDisposable.Dispose没有得到执行时,Finalize方法可以释放非托管资源,但是不会通知GC不要再调用Finalize方法。
注意,千万不要将Dispose方法设计成虚方法,也根本不存在base.Dispose()这样的调用。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 12:42

收获不少。
印象里finalize方法并不是一定会被GC调用的呀?      

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 16:03

@双鱼座
你刚才所描述的“千万不要将Dispose方法设计成虚方法,也根本不存在base.Dispose()这样的调用”我没有理解。

把Dispose(Boolean disposing)方法设计成虚函数是为了基类能通过重写这个方式实现自己的dispose逻辑。而且base.Dispose(disposing)这样调用也没有任何错误,难道你的基类就不用释放资源了吗?
在BCL里面FileStream类是这样实现dispose模式的:
protected override void Dispose(bool disposing)
{
try
{
if (((this._handle != null) && !this._handle.IsClosed) && (this._writePos > 0))
{
this.FlushWrite(!disposing);
}
}
finally
{
if ((this._handle != null) && !this._handle.IsClosed)
{
this._handle.Dispose();
}
this._canRead = false;
this._canWrite = false;
this._canSeek = false;
base.Dispose(disposing);
}
}

public virtual void Close()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

public void Dispose()
{
this.Close();
}

~FileStream()
{
if (this._handle != null)
{
this.Dispose(false);
}
}
这样该能说明问题了吧!!!!!!!!!!!!!

Sheva
     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 16:09

我发现一个问题,原来BCL也是这样用Dispose()方法的:
public virtual void Close()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
而我喜欢这样用:
public virtual void Close()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
两者区别就在于当Dispose(disposing)方法抛出一个异常的时候,也就是说该方法没有完成资源的释放,你是否希望让GC调用Finalize()方法来释放资源。
呵呵!!!这回我找到证据了,看来以后应该这样用dispose模式才对。

Sheva     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 16:32

@Sheva:
看来你是没有完全理解我的意思。
1.你不能拿BCL的代码说事儿,BCL可以调用GC的内部方法,以达到最高的性能。你行吗?
2.Dispose()方法和Dispose(bool disposing)方法都稍稍有一些特殊性,其特殊之处就在于Dispose中需要通知GC一些释放资源的策略。如果设计成虚方法就有可能重复或者遗漏(你必须确定由哪一级派生类负责通知GC)。
3.基类当然可能会释放一些非托管资源,你可以覆盖ReleaseUnmanageResources()方法(不在意你换一个方法名),这样每一级派生类仅释放自己申请的资源。这样的代码足够清晰。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-04 23:52

@双鱼座
“你不能拿BCL的代码说事儿,BCL可以调用GC的内部方法,以达到最高的性能。你行吗?”
呵呵,首先你得明白在CLR或是BCL里面没有什么所谓的secret APIs,BCL的类和你自定义的类没有任何区别,唯一的区别就在于BCL是微软写的,而自定义类是你自己写的。

你刚才的那个代码我却是没有看懂。
private void Dispose(bool disposing)
{
ReleaseUnmanageResources();
if (disposing)
{
GC.SuppressFinalize(this);
}
}
也就是说不管disposing参数的值是什么,ReleaseUnmanageResources()都将回被调用来释放资源,不知道是你写错了,还是你本来就没有明白GC的机制。当然我知道你确实是把ReleaseUnmanageResources()方法定义为虚函数了。

“其特殊之处就在于Dispose中需要通知GC一些释放资源的策略”
这个我也没有明白,你所说的策略是什么策略呀。首先我要告诉你当你使用了dispose模式时,你就告诉了GC,非托管资源的释放操作完全由你负责,而不是由Finalize方法负责,IDisposable.Dispose()方法和Finalize方法最大的不同就在于当你需要释放非托管资源的时候,你直接调用dispose方法来释放资源,而也就是说这种释放资源的操作是可控的,是deterministic的,而Finalize就不一样了,首先你不能直接调用该方法来释放非托管资源,因为这个方法只有GC才能调用,而如果你是靠GC来释放资源的话,这个释放操作就是不可控制的,因为GC只有当generation one满的时候才会进行释放资源的操作,而这种释放资源的方式从性能上来讲的话是不好的,因为GC首先会检查你的类是否定义了Finalize方法,然后把该类的一个root放到Finalization List里面,然后当下回再进行垃圾收集的时候,它才调用Finalize方法来释放资源,当Finalize方法被调用以后,你的这类就不能再被访问了。也就是说对于定义了Finalize 方法的类来说,GC需要进行两次垃圾收集才能对其释放非托管资源。这个楼主已经讲到,我这里只是再罗嗦两句。

还有,我还得必须指出的是咱们千万不要说BCL的类性能优秀是因为它可以访问一些CLR的内部APIs,这种猜想,这种假设是完全不符合逻辑,是没有任何根据的。但是我不得不承认有些BCL类确实和咱们的自定义类是不一样的,比如String, Int32, Int64, Delegate等,这些内建的类是非常特殊的类。他们之所以特殊是因为这些类要么和某一.NET编程语言息息相关,要么就是和CLR的内部操作息息相关,所以对这些类的性能,MS是做了特殊的工作的。但是对于其他的许许多多类比如我刚才提到的 FileStream类就和我们自定义的类没有任何区别。

BTW:言词有激烈之处,或是在某些概念上出错了,请大家斧正。


Sheva     

 re: 浅谈CLR的内存分配和回收机制 2006-03-05 12:25


轻松构建.net应用,
Asp.net空间只需155元/年。
磁盘空间: 50M
MS SQL数据库空间: 20M
支持: ASP.net、ASP
FTP管理
流量无限制
P4 2.0G以上CPU 2G内存
硬盘2*73G 操作系统: Windows2003
千兆光纤接入,百兆独享宽带
7×24小时全网监控系统,千兆防火墙系统、防攻击设备
网通/电信

查看更多虚拟主机请点击有更多款式配置适合你的需求。      

 re: 浅谈CLR的内存分配和回收机制 2006-03-05 14:27

@Sheva:
一向不喜欢做一些无谓的争论,可是我偏偏在网上遇到的都是一些热爱争论的人士。关于BCL中很多无法探知的秘密我的感受特别深刻,这根本不是什么猜想。BCL中太多的功能实现是提供给你用的而不是供你模仿的。你可以看到很多的extern方法打的都是internal类型有MethodImpl标签,这些方法没有提供任何外部调用。我这样的意思是关于Dispose方法是否设计为virtual,并不能从“FileStream中将Dispose(bool)设计成virtual”就让你获得了证据。我的代码如果你看不懂就好好看,总有一天你会看懂的。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-05 20:40

同意双鱼座的说法,如果把Dispose定义为Virtual的话,那么可能出现多次调用
GC.SuppressFinalize(this); 这个方法!
     

 re: 浅谈CLR的内存分配和回收机制 2006-03-05 23:18

@双鱼座
“我的代码如果你看不懂就好好看,总有一天你会看懂的”
这样说话名摆得不想交流嘛!
一切尽在不言中!

Sheva     

 re: 浅谈CLR的内存分配和回收机制 2006-03-05 23:35

@wzq:
是把Dispose(Boolean disposing方法设计为虚函数,而不是IDisposable.Dispose()方法,不管是我的这种实现方法还是双鱼座的实现方法,都需要一个虚函数,这点是一样的。而且GC.SuppressFinalize(this)这个方法只在IDisposable.Dispose()方法里面调用,只调用一次。

Sheva     

 re: 浅谈CLR的内存分配和回收机制 2006-03-06 17:06

@woodhead

GC只有对象重载实现了object.Finalize()以后才会在回收对象的时候调用该对象的Finalize()方法,避免不必要的性能损失。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-06 21:23

 

@双鱼座 and Sheva

GC并不关心用户类是否实现了System.IDisposable.Dispose()接口,更不会直接调用Dispose()来释放unmanaged resource。
 1 pubic UnManageResourceWrapper : System.IDisposable
 2 {
 3     private SomeComObject _com;
 4 
 5     public UnManageResourceWrapper()
 6     {
 7         this._com = new SomeComObject();
 8     }
 9 
10     static void Main(string[] args)
11     {
12         // initialize a com wrapper here
13         UnManageResourceWrapper wrapper = new UnManageResourceWrapper();
14         
15         // do something with the com object
16         浅谈CLR的内存分配和回收机制
17         
18         // damn~! forget to call the Dispose Method to release the inner com object
19     }
20 
21     void Dispose()
22     {    
23         // deal with the managed object
24         浅谈CLR的内存分配和回收机制
25 
26         // do something to rlease the inner com object
27         浅谈CLR的内存分配和回收机制
28 
29         // tell gc not to call the finalizer
30         GC.SuppressFinalize(this)
31     }
32 }
 1 pubic UnManageResourceWrapper : System.IDisposable, SomeUnManagedResourceBase
 2 {
 3     private SomeComObject _com;
 4 
 5     public UnManageResourceWrapper()
 6     {
 7         this._com = new SomeComObject();
 8     }
 9 
10     static void Main(string[] args)
11     {
12         // initialize a com wrapper here
13         UnManageResourceWrapper wrapper = new UnManageResourceWrapper();
14         
15         // do something with the com object
16         浅谈CLR的内存分配和回收机制
17         
18         // damn~! forget to call the Dispose Method to release the inner com object
19     }
20 
21     void Dispose()
22     {    
23         // release resource, either managed or unmanaged
24         this.Dispose(true);
25 
26         // tell gc not to call the finalizer
27         GC.SuppressFinalize(this)
28     }
29 
30     protected virtual void Dispose(bool disposing)
31     {
32         // called by Dispose() to release managed and unmanaged resource
33         if (disposing)
34         {
35             // deal with the managed object
36             浅谈CLR的内存分配和回收机制
37         }
38 
39         // do something to rlease the inner com object
40         浅谈CLR的内存分配和回收机制
41 
42         // call base.Dispose() to release unmanaged resource taken in base class
43         base.Dispose(disposing)
44     }
45 
46     ~UnManageResourceWrapper()
47     {
48         // just release the unmanaged resource,
49         // release managed object in Finalizer() may throw some exception which will cause Finalizer fails
50         this.Dispose(false);
51     }
52 }
 
     

 

 re: 浅谈CLR的内存分配和回收机制 2006-03-06 21:25

发现msdn上说得更好,

<a href="ms-help://MS.NETFrameworkSDKv1.1.CHS/cpguidenf/html/cpconimplementingdisposemethod.htm">实现 Dispose 方法</a>     

 re: 浅谈CLR的内存分配和回收机制 2006-03-14 15:21

Dispose是实是虚没有什么大关系,根据你的Design Pattern不同而不同。或许你有更好的Design Pattern,不用Dispose也行呢!

但是,在MS所推荐的设计模式里,的确是存在一实一虚两个Dispose方法的:
1,public void Dispose()
2,protected virtual void Dispose(bool disposing)

对于此,MSDN是这么解释的:
public的方法,是给你的用户调用(假设你编写的是一个控件,那么就是使用你控件的人)
protected的方法,是你内部调用(供Close之类,以及Finalizer调用),以及供子孙后代调用

对于1,道理很简单,因为用户肯定希望能够Dispose你的组件,所以我们要提供一个public的Dispose方法;
对于2,2被用在两种场合:一种是由Finalizer调用。我们虽然尽量避免使用Finalizer,但是Finalizer是唯一100%会在终结对象时被调用的方法,万一Dispose没有被调用,它还能作为最后一道防线释放资源。因此Dispose(false)被用在Finalizer的场合;另一种就是内部、子孙调用,这时候使用Dispose(true)的形式。
(具体这两种形式有何差别,简而言之,true的时候释放Unmanaged资源、并通知其他相关的Managed对象进行Dispose;false的时候只释放Unmanaged资源。详细的参阅MSDN)

到这里我们就看出来了,如果按照MSDN的模式走,就必须设置一个虚的Dispose,否则基类的Unmanged资源得不到主动释放,最后只能被Finalizer释放。     

 re: 浅谈CLR的内存分配和回收机制 2006-03-14 22:08

@smalldust:
听起来你好象是懂了,不过我还是不能确信你真的懂了。
那个disposing形参的意义非常明确,决不仅仅是用于定义一个Dispose的重载版本(如果是这样我宁可重新定义一个方法)。从Finalize调用就传实参false;从IDisposable.Dispose调用就传实参true,告诉是谁在调用这个方法,已经很清楚地表明了这是两种不同的调用。其区别之一我在我上面的回复中已经讲了,就是决定是否通知GC取消执行Finalize。这样的伎俩决不仅仅是IDisposable中才会用到,也不只是从MSDN才能学到这样的伎俩。       

#  re: 浅谈CLR的内存分配和回收机制 2006-03-03 15:52 A.Z