面向对象——垃圾回收

一、垃圾回收


托管堆的工作方式非常类似于栈,在某种程度上,对象子内存中一个挨一个地放置,这样很容易指向下一个空间存储单元的堆指针,来确定下一个对象的位置。


  在垃圾回收器运行时,它会从堆中删除不再引用的对象。在完成删除操作后,堆会立即把对象分散开来,于已经释放的内存混合在一起,如图:


如果托管的堆也这样的话,在其上给新对象分配内存就成为一个很难处理的过程,运行库必须搜索整个堆,才能找到足够大的内存块来存储每个新对象。但是,垃圾回收器不会让堆处于这种状态。只要他释放了能连续的内存块。因此,堆可以继续想栈那样确定在什么地方存储新对象。当然,在移动对象时,这些对象的所有引用都需要用正确的新地址来更新,但垃圾回收器也会处理更新问题。


垃圾回收器的这个压缩操作是托管的堆也非托管的旧堆得区别所在。使用托管的堆,就需要读取堆指针的值即可,而不需遍历地址的链表,来查找一个地方来放置新数据。因此,在.NET下实例化对象要快得多。有趣的是,访问他们也比较快,因为对象会压缩到堆上相同的内存区域,这样需要交换的页面较少。Microsoft相信,尽管来及回收器需要做一些工作,压缩堆,修改它移动的所有对象引用,致使性能降低,但这些性能会得到更多的弥补。


注:一般情况下,垃圾收集器在.NET运行库认为需要它时运行。可以调用System.GC.Collect()方法,强迫垃圾回收器在代码的某个地方运行,System.GC类是一个表示垃圾回收器的.NET类,Collect()方法初始化垃圾回收器。但是,GC类使用的场合很少,例如,代码中有大量的对象刚刚取消引用,就适合调用垃圾收集器。但是,垃圾会收器的逻辑不能保证在一次垃圾收集过程中,所有未引用的对象从堆中删除。


垃圾会收器运行时,它实际上会降低应用程序的性能,因为在垃圾会收器完成其任务之前,应用程序不可能继续运行。使用.NET垃圾回收器可以减少这个问题的影响,因为它是一个世代(generational)垃圾回收器。


创建对象时,会把这些对象放在托管堆上。堆的第一部分成为第0代。创建对象时,会把它们移动到堆得这个部分中。因此,这里驻留了最新的对象。


对象继续放在这个部分,直到第一个对象集合在垃圾回收的过程中生成。这个清理过程之后仍保留的对象被压缩,然后移动到堆的下一部分或世代部分——第一代对应的部分。


此时,第0代对应的部分上。老对象的这个移动会再次发生。接着重复下一次回收过程。这意味着,第1代中在垃圾回收过程中遗留下来的对象会移动到堆的第2代,位于第0代的对象会移动到第1代,第0代仍用于放置新对象。


注:有趣的是,在给对象分配内存空间时,如果超出了第0代对应的部分的容量,或者调用了GC.Collect()方法,就会进行垃圾回收。


这个过程极大的提高了应用程序的性能。一般而言,最新的对象通常是可以回收的对象,而且可能也会回收大量的比较新的对象。如果这些对象在堆中地 位置是相邻的,垃圾回收过程就会更快,另外,相关的对象相邻放置也会使程序执行得更快。在.NET中,垃圾回收提高性能的另一个领域是架构处理堆上较大对象的方式。在.NET下,较大独享有自己的托管堆,成为大对象堆。使用大于85000个字节的对象时,他们就会放在这个特殊的堆上,而不是主堆上。.NET应用程序不知道这两者的区别,因为这是自动完成的。其原因是在堆上压缩大对象是比较昂贵的,因此驻留在大对象上的对象不执行压缩过程。



二、释放非托管的资源


垃圾回收器的出现意味着,通常不需要担心不在需要的对象,只要这些对象的所有引用都超出作用域,并允许垃圾回收器在需要的时释放内存即可。虽然,垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不知道如何释放非托管资源(例如文件句柄,网络连接,数据库连接)。常见的非托管资源包括:ApplicationContext、Brush、Component、ComponentDesigner、Container、Context、Cursor、FileStream、Font、Icon、Image、Matrix、object、OdbcDataReader、OleDBDataReader、Pen、Regex、Socket、StreamWriter、Timer、Tooltip等。托管类在封装对非托管资源的直接或间接引用时,需要制定专门的规则,确保非托管的资源在回收类的一个实例时释放。


在定义一个类时,可以使用两种机制自动释放非托管的资源,这些机制常常放在一起实现,因为每种机制都为问题提供了略微不同的解决方法。这两种机制是:


声明一个析构函数(或终结器),作为类的一个成员。

在类中事项System.IDisposable接口。

第一种 析构函数


C#的析构函数在底层的.NET体系中,这些函数成为终结器(finalizer),在C#定义析构函数时,编译器发送给程序集的实际上是Finalize()方法。它不会影响源代码,但如果需要查看程序集的内容,就应直到这个事实。析构函数的语法类似一个方法,与包含的类同名,但有一个前缀(~),不带参数,没有访问修饰符。例如:


Class


{

          ~MyClass()


           {

                            //destructor implementation


}


}


C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于Finalize()方法的代码,从而确保执行父类的Finalize()方法。例如:


 Protected override void Finalize()


{

          Try


{

        //destructor implementtion


}


Finally


{

        Base.Finalize();


}


}


如上所示,在~MyClass()析构函数中实现的代码封装在Finalize()方法的一个try块中。对父类的Finalize()方法的调用放在finally块中,确保该调用的执行。


由于使用C#时,垃圾回收器的工作方式,无法确定C#对象的析构函数何时执行。所以,不能再析构函数中放置需要在某一时刻运行的代码,也不应使用能以任意顺序对不同的实例调用的析构函数。如果对象占用了宝贵而重要的资源,此时就不能等待垃圾回收器来释放了。C#的析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾回收器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能销毁;第一次调用析构函数时,没有删除对象,第二次调用时才能真正删除对象。另外、运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数,而且使用它们执行长时间的清理任务,堆性能的影响就会非常显著。


第二种 IDisposable接口


在C#中,推荐使用System.IDisposable接口代替析构函数。IDisposable接口定义了一种模式(具有语言及的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void,MyClass类的Dispose()方法的实现代码如下:


Class MyClass():IDisposable


{

     Public void Dispose()


     {

   //implementation


}


}


Dispose()方法的实现代码显示的释放由对象直接使用的所有非托管资源,并在所有也实现IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管提供了精确的控制。


        假定有一个ResourceGobble类,它依赖于使用某些外部资源,且实现IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码:


ResourceGobble theInstance=new ResourceGobble();


//do yourProcessing


theInstance.Dispose();


        但是,如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所有应使用try块,例:


ResourceGobble theInstance=null;


Try


{

        theInstance=new ResourceGobble();


                  //代码


}


Finally


{

        If(theInstance!=null)


        {

                  theInstance.Dispose();


}


}


即使在处理过程中出现了异常,这个版本本也可以确保总是在theInstance上调用Dispose()方法,总是释放theInstance使用的任意资源。但是,如果总是这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在实现IDisposable接口对象的引用超出范围作用域时,在该对象上自动调用Dispose()方法。该语法使用using关键字来完成这一工作。例:


Using(ResourceGobble theInstance=new ResourceGobble())


{

        //代码


}


Using 语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量放在随后的语句块。另外,在变量超出作用域时,即使出现异常,也会自动调用Dispose()方法。然而,如果已经使用try块来捕获其他异常,就会比较清晰,如果不使用using语句,仅在已有的try块的finally子句中调用Dispose()方法,就可以避免额外的代码缩进。


注:对于某些类,使用Close() 方法要比Dispose()方法更富有逻辑性,例如,在处理文件或数据库连接时就是这样。在这种情况下,常常实现IDisposable()接口,再实现一个独立的Close()方法。这种方法在类的使用上比较清晰,还支持C#提供的using语句。


三、总结


一般情况下,最好实现这两种机制,获得这两种机制的优点,克服缺点。假定大多数程序员都能正确的调用Dispose()方法,同时把实现析构函数作为一种安全机制,以防没有调用Dispose()方法。下面就是一个双重的例子:


   public class SampleClass : IDisposable


   {

       //演示创建一个非托管资源


       private IntPtr nativeResource = Marshal.AllocHGlobal(100);


       //演示创建一个托管资源


       private AnotherResource managedResource = new AnotherResource();


       private bool disposed = false;


       /// <summary>


       /// 实现IDisposable中的Dispose方法


       /// </summary>


       public void Dispose()


       {

           //必须为true


           Dispose(true);


           //通知垃圾回收机制不再调用终结器(析构器)


           GC.SuppressFinalize(this);


       }


       /// <summary>


       /// 必须,以备程序员忘记了显式调用Dispose方法


       /// </summary>


       ~SampleClass()


       {

           //必须为false


           Dispose(false);


       }


       /// <summary>


       /// 非密封类修饰用protected virtual


       /// 密封类修饰用private


       /// </summary>


       /// <param name="disposing"></param>


       protected virtual void 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;


       }


       public void SamplePublicMethod()


       {

           if (disposed)


           {

               throw new 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>


       protected virtual void Dispose(bool disposing)


       {

           //省略代码


       }


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


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


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


       public void Dispose()


       {

           //必须为true


           Dispose(true);


           //其他省略


       }


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


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


       ~SampleClass()


       {

           //必须为false


           Dispose(false);


       }


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


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


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


四、引用类型赋值为null与加速垃圾回收


在标准的Dispose模式中(见前一篇博客“C#中标准Dispose模式的实现”),提到了需要及时释放资源,却并没有进一步细说让引用等于null是否有必要。


有一些人认为等于null可以帮助垃圾回收机制早点发现并标识对象是垃圾。其他人则认为这没有任何帮助。是否赋值为null的问题首先在方法的内部被人提起。现在,为了更好的阐述提出的问题,我们来撰写一个Winform窗体应用程序。如下:


   private void button1_Click(object sender, EventArgs e)


       {

           Method1();


           Method2();


       }


       private void button2_Click(object sender, EventArgs e)


       {GC.Collect();}      


       private void Method1()


       {

           SimpleClass s = new SimpleClass("method1");


           s = null;


           //其它无关工作代码(这条注释源于回应回复的朋友的质疑)


       }


       private void Method2()


       {

           SimpleClass s = new SimpleClass("method2");


       }


   }


   class SimpleClass


   {

       string m_text;


       public SimpleClass(string text)


       {

           m_text = text;


       }


       ~SimpleClass()


       {

           MessageBox.Show(string.Format("SimpleClass Disposed, tag:{0}", m_text));


       }


   }


先点击按钮1,再点击按钮2释放,我们会发现:q 方法Method2中的对象先被释放,虽然它在Method1之后被调用;q 方法Method2中的对象先被释放,虽然它不像Method1那样为对象引用赋值为null;


在CLR托管应用程序中,存在一个“根”的概念,类型的静态字段、方法参数以及局部变量都可以作为“根”存在(值类型不能作为“根”,只有引用类型的指针才能作为“根”)。


上面的两个方法中各自的局部变量,在代码运行过程中会在内存中各自创建一个“根”.在一次垃圾回收中,垃圾回收器会沿着线程栈上行检查“根”。检查到方法内的“根”时,如果发现没有任何一个地方引用了局部变量,则不管是否为变量赋值为null,都意味着该“根”已经被停止掉。然后垃圾回收器发现该根的引用为空,同时标记该根可被释放,这也表示着Simple类型对象所占用的内存空间可被释放。所以,在上面的这个例子中,为s指定为null丝毫没有意义(方法的参数变量也是这种情况)。


更进一步的事实是,JIT编译器是一个经过优化的编译器,无论我们是否在方法内部为局部变量赋值为null,该语句都会被忽略掉:


?


s = null;


在我们将项目设置为Release模式下,上面的这行代码将根本不会被编译进运行时内。


正式由于上面这样的分析,很多人认为为对象赋值为null完全没有必要。但是,在另外一种情况下,却要注意及时为变量赋值为null。那就是类型的静态字段。为类型对象赋值为null,并不意味着同时为类型的静态字段赋值为null:


   private void button1_Click(object sender, EventArgs e)


       {

           SimpleClass s = new SimpleClass("test");


       }


       private void button2_Click(object sender, EventArgs e)


       {

           GC.Collect();


       }


   }


   class SimpleClass


   {

       static AnotherSimpleClass asc = new AnotherSimpleClass();


       string m_text;


       public SimpleClass(string text)


       {

           m_text = text;


       }      


       ~SimpleClass()


       {

           //asc = null;


           MessageBox.Show(string.Format("SimpleClass Disposed, tag:{0}", m_text));


       }


   }


   class AnotherSimpleClass


   {

       ~AnotherSimpleClass()


       {

           MessageBox.Show("AnotherSimpleClass Disposed");


       }


   }


以上代码运行的结果使我们发现,当执行垃圾回收,当类型SampleClass对象被回收的时候,类型的静态字段asc并没有被回收。


必须要将SimpleClass的终结器中注释的那条代码启用。


字段asc才能被正确释放(注意,要点击两次释放按钮。这是因为一次垃圾回收会仅仅首先执行终结器)。之所以静态字段不被释放(同时赋值为null语句也不会像局部变量那样被运行时编译器优化掉),是因为类型的静态字段一旦被创建,该“根”就一直存在。所以垃圾回收器始终不会认为它是一个垃圾。非静态字段不存在这个问题。将asc改为非静态,再次运行上面的代码,会发现asc随着类型的释放而被释放。


上文代码的例子中,让asc=null是在终结器中完成的,实际工作中,一旦我们感觉到自己的静态引用类型参数占用内存空间比较大,并且使用完毕后不再使用,则可以立刻将其赋值为null。这也许并不必要,但这绝对是一个好习惯。试想一下在一个大系统中,那些时不时在类型中出现的静态变量吧,它们就那样静静地呆在内存里,一旦被创建,就永远不离开,越来越多,越来越多……。


上一篇:(五)C++基本类型表


下一篇:windows DNS服务器与防火墙的设置