阿宽

Nothing is more powerful than habit!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.net无用单元回收

Posted on 2007-07-11 11:39  宽田  阅读(329)  评论(0编辑  收藏  举报

       我们平常写程序很少自己去写资源管理的,除非写非常大型的应用程序,或者大公司自己的sdk。看到过PGP源代码的一定知道,PGP的SDK就实现了自己的内存管理。自己管理内存烦恼实在多多,忘记释放了,释放了又再次访问的bug层出不穷,这种bug又非常难查。普通的逻辑bug,简单测试发现程序没有按照预想的运行就可以找到。但是内存的问题却很难发现。过去很多公司也为解决这方面的问题作过很大努力,比如Compuware的BoundsChecker,Rational的Purify。这些工具使用起来也很困难,经常找到的点都是你用的开发库的代码。现在.NET提供了全套的资源管理(无用资源回收:Garbage Collection,简称GC),能够让我们从中解脱出来,把精力用在自己应该解决的业务问题上去。当然它不是万能的,我们最好多了解他的原理,以便我们可以更好的享用它。

系统资源不是无限的,内存用完要释放,文件,网络连接都是系统资源,用完之后也要释放。在面向对象的系统中,所有的东西都是对象,所以使用任何资源都要从系统分配内存,最后释放它。使用资源的过程无外乎五个步骤:

1.         为代表资源的类型分配内存;

2.         初始化资源状态,请求非内存系统资源(如打开文件,建立网络连接等);

3.         通过访问类型的实例(对象)及其成员变量、方法等来访问资源;(可能多次)

4.         清空资源状态,释放系统资源(如关闭文件,关闭网络连接等);

5.         释放内存

我们遇到的内存问题一般都在上面的五个步骤中,.NET提供的无用单元回收(GC)机制基本上都可以解决这些问题。不过GC是不知道如何清空资源状态和释放系统资源的(即上面的第四步),这就要利用到Fina

CLR实现了一个托管堆(Managed Heap),要求所有的资源都必须从这个堆中分配,并且无需释放。下面就详细说明对象是如何在堆中分配,GC又是如何做无用单元回收的。

CLR在进程初始化时,保留一块连续的内存,这块连续的内存就是托管堆。CLR同时为托管堆维护一个指针,这个指针永远指向下一个可以分配的内存空间,我们这里叫NextObjPtr。

当程序适用new创建一个对象时,new首先确认堆中是否有足够的内存空间,如果有的话,则为对象分配空间,调用对象的构造函数,返回分配空间的地址,接着NextObjPtr指向剩余空间的地址,即下一个可以分配的地址。如下图:

图中虚线是NextObjPtr的起始地址,分配对象C成功并返回地址后,NextObjPtr移到实线的位置。

再 让我们看看普通应用程序的堆内存分配方式。普通的内存分配方式维护一个空闲内存列表,系统首先遍历空闲空间列表,找到一个足够大的空间,然后将其拆分出足 够的空间分配,然后再将剩下的空间加入到空闲空间列表。在历史上有很多的实现进程堆内存分配的算法,比如比较著名的二分法等等。但是比较来看,.NET的内存分配方法要快的多。

不过内存不是无限的,堆的空间分配光了怎么办?CLR在分配内存的时候,如果发现堆中的空闲空间不足时,就会启动无用空间回收。GC将堆中不再被使用的对象占用的内存释放掉,然后将堆整理,使其剩下连续的空间以待分配,当然如果没有可以释放的对象或者释放后内存还是不够的话,就抛出OutOfMemoryException异常。GC是如何断定一个对象不再被使用了呢?

每一个应用都有一组根,这些根包括了标示托管堆中一组对象的存储单元。被认为是根的对象包括:

1.         所有的全局和静态对象;

2.         一个线成栈中的局部对象和函数的参数;

3.         任何CPU寄存器包含的指针指向的对象;

上面根的列表由JIT和CLR维护并且GC可以访问。

开始无用单元回收后GC就开始遍历根,找到第一个根指向的对象,沿着这个对象向下找,找到这个对象所有引用的对象,以及引用之引用,并将其放入到一个集合中。这一个完成后,就接着找下一个根。一旦GC发现某个对象已经在集合中,就停止这个分支的搜寻以防止重复和引用死循环。

完成后,GC就有了一个所有根可以访问到的对象的集合,这个集合中没有的对象就认为是无用的。如下图:

GC的集合包括了对象A、B、D、F,而对象C、E、G就是无用对象,GC将释放其资源。GC接着遍历堆中的所有对象,释放无用对象,并将有用对象向内存的地位移动(据说使用的memcpy),以保证空闲空间的连续性。NextObjPtr就被指向空闲空间的开始地址。这样做会使一些对象的引用地址失效,GC负责更正这些指针。回收后的堆如下图:

这一次回收所作的工作不可谓不复杂,消耗的CPU时间也是很多。不过还好,它不是时时刻刻都在运行,而是只在堆满了之后才回收(实际上是Generation 0满了之后,Generation我将在接下来的文章讨论),其他分配内存的时候还是非常快的。而且.NET提供丰富的设置来提高无用单元回收的效率。

FinaLize

在上一篇文章中我分配使用资源一共五步,我们已经知道了GC是如何释放无用对象的内存了。但是它怎么实现第四步清空资源使用状态、释放利用到的一些非内存的系统资源呢?.NET引入了Fina

GC在无用单元回收时一旦发现某个对象有Fina

我们可以用两种方法来写自己的Fina

代码1

pub

{

   pub

   {

   }

 

   protected override void Fina

   {

      Console.WriteLine(“Fina

   }

}

使用这种方法时要注意一点,.NET不会帮你做调用基类的Fina

代码2

pub

{

   pub

   {

   }

 

   protected override void Fina

   {

      Console.WriteLine(“Fina

      base.Fina

   }

}

另外一种方法就是析构函数。C#中的析构函数不同于C++。我们看下面的代码:

代码3

pub

{

   pub

   {

   }

 

   ~SomeClass()

   {

      Console.WriteLine(“Fina

   }

}

它等同于代码2。

使用Fina

GC是如何实现Fina

当托管堆的内存不足的时候,GC开始对堆进行回收。GC回收一个对象前,先检查Fina

对象G和对象E不在根的范围之内,被回收。对象F和对象C由于需要Fina

这时对象F和对象C不再是根的一部分,如果此时GC进行回收,将会被认作无用对象进行回收,回收后如下图:

上面简单描述了Fina

l         Generation

每次都对整个对进行搜索,压缩是非常耗时的。微软总结了一些过去的开发中出现的现象,其中有一条就是,越是新的对象,越是最快被丢弃不再使用。微软根据这个经验在内存回收中引入了Generation的概念,我此处暂时将其翻译成代。托管堆开始的时候是空的,程序启动开始在其中分配对象,这时候的对象就是第0代(Generation 0)对象。如下图:

接下来,到托管堆空间不足,GC进行了第一次回收,剩下的没有被回收的对象就升为第一代,之后再新分配的对象就是第0代(图甲)。再之后GC再进行回收的话只回收第0代,未被回收的第0代升级为第一代,原来的第一代升级为第0代(图乙)。

GC缺省的代(Generation)最高就是2,升级到第二代就不会再升级了。那什么时候GC回收第一,第二代呢?当GC回收完第0代后,发现内存空间还不够,就会回收第一代,回收完第一代,还不够,就回收第二代。

这一篇也写了不少了,所以下一篇再继续,下一篇写WeakReference和如何在自己的代码中控制GC的动作。

这篇文章接着上一次的来,继续讨论无用资源回收的其它一些话题。

l         WeakReference(弱引用)

我们平常用的都是对象的强引用,如果有强引用存在,GC是不会回收对象的。我们能不能同时保持对对象的引用,而又可以让GC需要的时候回收这个对象呢?.NET中提供了WeakReference来实现。弱引用使用起来很简单,看下面的代码:

代码1

Object obj = new Object();

WeakReference wref = new WeakReference( obj );

obj = null;

第一行代码新建了一个新的对象,这里叫它对象A,obj是对对象A的强引用。接着第二行代码新建了一个弱引用对象,参数就是对象A的强引用,第三行代码释放掉对对象A的强引用。这时如果GC进行回收,对象A就会被回收。

怎样在取得对象A的强引用呢?很简单,请看代码2:

代码2

Object obj2 = wref.Target;

if( obj2 != null )

{

   … // 做你想做的事吧。

}

else

{

   …// 对象已经被回收,如果要用必须新建一个。

}

只要显示的将弱引用的Target属性附值就会得到弱引用所代表对象的一个强引用。不过在使用对象之前要对其可用性进行检查,因为它可能已经被回收了。如果你得到的是null(VB.NET下为Nothing),表明对象已经被回收,不能再用了,需要重新分配一个。如果不是null,就可以放心大胆的用了。

接下来让我们看WeakReference的另外一个版本,请看代码3:

代码3

// pub
            
            

//   object target,

//   bool trackResurrection

//);

Object obj1 = new Object();

Object obj2 = new Object();

WeakReference wref1 = new WeakReference( obj1, false );

WeakReference wref2 = new WeakReference( obj2, true );

WeakReference的另外一个版本有两个参数,第一个参数和我们前面用的版本的一样。第二个参数让我们看一下他的原型,bool trackResurrection,跟踪复活,是个bool型,就是是否跟踪复活。前面的文章中我提到过需要Fina

现在让我们看看WeakReference是如何实现的。很显然WeakReference不能直接的引用目标对象,WeakReference的Target属性的get/set是两个函数,从某处查到目标对象的引用返回,而不是我们最常用写的那样直接返回或者设置一个私有变量。GC维护了两个列表来跟踪两种弱引用的目标对象,在一个WeakReference对象创建时,它在相应的列表中找到一个位置,将目标对象的引用放入,很显然,这两个列表不是根的一部分。在GC进行内存回收的时候,如果要回收某一个对象,会检查弱引用的列表,如果保存着这个对象的引用,则将其设为null。

l         控制GC行为

.NET提供了System.GC类来控制GC的行为,GC只提供静态方法,无需也不能(GC的构造方法被做成私有)创建它的实例。

GC类提供的最主要一个方法就是Collect,它使自己控制内存回收成为可能。Collect方法有两种版本,void Collect(); 和 void Collect(int);。第二个版本的Collect提供一个参数,让你选择是回收那一代(Generation)以及比其年轻者的对象,也就是说GC.Collect(0)只回收第0代的对象,而GC.Collect(1)则是要回收第0代和第一代的对象。Collect()则是回收所有对象,等同于GC.Collection(GC.MaxGeneration)。MaxGeneration是GC唯一的一个属性,它给出GC的最高代。

GC类提供了另外一个方法来获取某个对象的代值,GetGeneration。代码4给出了一段例子代码,可以让我们更好的理解Generation和GC提供的这两个方法。请看代码4:

代码4

class GCDemoClass

     {

         ~GCDemoClass()

         {

              Console.WriteLine( "Demo Class Fina

         }

}

static void

         {

              GCDemoClass inst = new GCDemoClass();

 

              Console.WriteLine( "Generation of demo object:{0} ",  GC.GetGeneration( inst ) );

 

              GC.Collect();

 

              Console.WriteLine( "Generation of demo object:{0} ",  GC.GetGeneration( inst ) );

 

              GC.Collect();

 

              Console.WriteLine( "Generation of demo object:{0} ",  GC.GetGeneration( inst ) );

 

              inst = null

             

              GC.Collect( 0 );

              Console.WriteLine( " After collect generation 0 ..." );

 

              GC.Collect( 1 );

              Console.WriteLine( " After collect generation 1 ... " );

 

              GC.Collect( 2 );

              Console.WriteLine( " After collect generation 2 ... " );

 

              Console.ReadLine();

         }

GCDemoClass实现了一个析构函数,根据我前面文章提到的,编译器会将其变为Fina

GC还提供了其他的一些方法,这里就不再讨论了,大家可以去看MSDN。


                                                                                                                        轉自:http://www.upschool.com.cn/edu/1317/2005/528/10du246616_1.shtml