二、线谈C#内存回收与Dispose﹐Close﹐Finalize方法

实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用 Finalize 方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。
原文:http://msdn2.microsoft.com/zh-cn/library/0s71x931(VS.80).aspx
    英文:把zh-cn替成en-us。此文档对应.net2.0,把VS.80替成VS.90可查看.net3.5最新文档。两者无甚差别,可见自.net1.1之后,垃圾回收机制没有改变。

    据上文引用,关于GC的二次回收,作图一张,如下:
 
为了验证GC对含有终结器对象的两次回收机制,我写了一个例子测试,代码如下:

using  System;
using  System.Threading;
using  System.IO;
using  System.Data.SqlClient;
using  System.Net;

namespace  Lab
{
    
class Log
    
{
        
public static readonly string logFilePath = @"d:log.txt";

        
public static void Write(string s)
        
{
            Thread.Sleep(
10);
            
using (StreamWriter sw = File.AppendText(logFilePath))
            
//此处有可能抛出文件正在使用的异常,但不影响测试
            {
                sw.WriteLine(
"{0} TotalMilliseconds:{1} TotalMemory:{2}", s, 
                    DateTime.Now.TimeOfDay.TotalMilliseconds, GC.GetTotalMemory(
false));
                sw.Close();
            }

        }

    }


    
class World
    
{
        
protected FileStream fs = null;
        
protected SqlConnection conn = null;

        
public World()
        
{
            fs 
= new FileStream(Log.logFilePath, FileMode.Open);
            conn 
= new SqlConnection();
        }


        
protected void Finalize()
        
{
            fs.Dispose();
            conn.Dispose();
            Log.Write(
"World's destructor is called");
        }

    }


    
class China : World
    
{
        
public China()
            : 
base()
        
{

        }


        
~China()
        
{
            Log.Write(
"China's destructor is called");
        }


    }


    
class Beijing : China
    
{
        
public Beijing()
            : 
base()
        
{

        }


        
~Beijing()
        
{
            Log.Write(
"Beijing's destructor is called");
        }

    }

}


using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Data.SqlClient;

namespace  Lab
{
    
class Program
    
{
        
static void Main(string[] args)
        
{
            TestOne();
            Log.Write(
"In Main.. ");
        }


        
static void TestOne()
        
{
            
new Beijing();

            GC.Collect();
            GC.WaitForPendingFinalizers();
            Log.Write(
"In TestOne.. ");
        }

    }

}

F5执行一下,返回如下结果:
Beijing ' s destructor is called  TotalMilliseconds:53009847.4384    

TotalMemory:
701044
China
' s destructor is called    TotalMilliseconds:53009857.4528    

TotalMemory:
717428
World
' s destructor is called    TotalMilliseconds:53009867.4672   

 TotalMemory:
733812
In TestOne..                    TotalMilliseconds:
53009877.4816     TotalMemory: 758388
In Main..                       TotalMilliseconds:
53009887.496      TotalMemory: 783056

Beijing
' s destructor is called  TotalMilliseconds:53589020.248     TotalMemory:697744
China ' s destructor is called    TotalMilliseconds:53589030.2624    TotalMemory:714128
World ' s destructor is called    TotalMilliseconds:53589040.2768    TotalMemory:738704
In TestOne..                    TotalMilliseconds: 53589050.2912     TotalMemory: 763280
In Main..                       TotalMilliseconds:
53589060.3056     TotalMemory: 779664

注:重点看时间与内存变化,以下同。

    WaitForPendingFinalizers()相当于join了终结器队列执行线程。派生类Beijing及其父类的终结器 确实已经成功执行,但是为什么 内存占用却 不降反升?结合两次结果来看,垃圾回收器真正释放内存应该是在退出当前应用程序域之后发生的。msdn2中有云:
在应用程序域的关闭过程中,对没有免除终结的对象将自动调用Finalize,即使那些对象仍是可访问的。
    看来msdn此言非虚

    但是为什么内存没有真正被GC回收呢?World的终结器既已执行,其中fs.Dispose()与conn.Dispose()也得以成功执行,为什么就连微软鼓励使用的Dispose()也不好使了呢?是fs与conn对象不占内存,差别微乎其微吗?为了验证是与不是,把上文例码中的fs与conn的相关定义及初始化代码一并去掉。再运行一下:
Beijing ' s destructor is called  TotalMilliseconds:54514090.4336    TotalMemory:566124
China ' s destructor is called    TotalMilliseconds:54514100.448     TotalMemory:582508
World ' s destructor is called    TotalMilliseconds:54514110.4624    TotalMemory:598892
In TestOne..                    TotalMilliseconds: 54514120.4768     TotalMemory: 623468
In Main..                       TotalMilliseconds:
54514130.4912     TotalMemory: 639852

Beijing
' s destructor is called  TotalMilliseconds:56343741.3424    TotalMemory:563252
China ' s destructor is called    TotalMilliseconds:56343751.3568    TotalMemory:579636
World ' s destructor is called    TotalMilliseconds:56343761.3712    TotalMemory:596020
In TestOne..                    TotalMilliseconds: 56343771.3856     TotalMemory: 620596
In Main..                       TotalMilliseconds:
56343781.4        TotalMemory: 636980

  内存占用明显减少,看样子没有冤枉GC。让它回收,它确实没有给我干活啊。

    在C#中,如果一个自定义类没有构造器,编译器会添加一个隐藏的无参构造器。但是析构函数不会自动创建。一旦析构函数创建了,终结器也便自动产生了。构构函数其实等同于如下代码:
try {
    Finalize();
}
finally {
    
base.Finalize();
}
在上文代码中,World类虽没有析构函数,也被派生类China的析构触发得以执行。

    如果在派生类中不存在析造函数,却重载了基类的终结器,如下:
protected override void Finalize(){...}
    垃圾回收时,GC找不到构造函数,会直接调用终结器。因终结器已重写,如果在该终结器中不得调用基类的终结器,那么GC将忽略基类。可以利用这个特性写一个不受垃圾回收器管辖的类,以实现某种特殊的效果。此乃旁边左道,与高手见笑了。

    对于上文代码,如果把TestOne函数改成如下:
static   void  TestOne()
{
     Beijing bj 
= new Beijing();
     GC.Collect();
     Log.Write(
"In TestOne.. ");
}
    运行一下,GC貌似无用,bj及其父类的析构函数依然在Log.Write("In TestOne...")之后执行,有无WaitForPendingFinalizers()无甚差别。但如果只new一下对象,并不赋值于变量,code如下:
static   void  TestOne()
{
     
new Beijing();
     GC.Collect();
     Log.Write(
"In TestOne.. ");
}
    运行结果如下:
Beijing ' s destructor is called  TotalMilliseconds:59773883.6448    TotalMemory:562036
In TestOne..                    TotalMilliseconds: 59773883.6448     TotalMemory: 586612
China
' s destructor is called    TotalMilliseconds:59773893.6592    TotalMemory:602996
In Main..                       TotalMilliseconds: 59773893.6592     TotalMemory: 619380
World
' s destructor is called    TotalMilliseconds:59773903.6736    TotalMemory:635764

Beijing
' s destructor is called  TotalMilliseconds:59775696.2512    TotalMemory:561080
China ' s destructor is called    TotalMilliseconds:59775706.2656    TotalMemory:577464
In TestOne..                    TotalMilliseconds: 59775706.2656     TotalMemory: 602040
World
' s destructor is called    TotalMilliseconds:59775716.28      TotalMemory:618424
In Main..
不用WaitForPendingFinalizers()也触发了Beijing及其父类的析构函数。GC.Collect是异步调用,该问一代而过。至于Log.Write执行的先后,要看谁能获得log文件操作句柄。上文写log文件的代码有问题,多线程应用中可能引发文件正在使用的异常,实际应用中应先申请文件句柄,申请成功lock之后方可操作。由于Write方法中让线程沉睡了10毫秒,故GC在此空档内有机会获得了文件操作句柄。

    有两种情况GC都是可以触发对象的析构函数的:
    1,如前面所说,在退出当前应用程序域时。
    2,当对象不能再被访问时。若只是new一个对象,转行便满足条件。

    对于GC.Collect,有两个版本:
    1,GC.Collect();
    2,GC.Collect(int32);参数为Generatio。什么是Generation?
    在.Net中,创建对象所用内存在托管堆中分配,垃圾管理器也只管理这个区域。在堆中可配.Net分配的内存,被CLR以块划分,以代[Gemeration]命名,初始分为256k、2M和10M三个代(0、1和2)。并且CLR可以动态调整代的大小,至于如何调整,策略如何不甚清楚。在堆创建的每一个对象都有一个Generation的属性。.Net约定,最近创建的对象,其Generation其值为0。创建时间越远代数越高,下面的代码可以说明这一点:
using  System;
using
 System.Collections.Generic;
using
 System.Text;
using
 System.Data.SqlClient;

namespace
 Lab
{
    
class
 Program
    
{
        
static void Main(string
[] args)
        
{
            TestObject obj 
= new
 TestObject();
            
int generation = 0
;

            
for (int j = 0; j < 6; j++
)
            
{
                generation 
=
 GC.GetGeneration(obj);
                Console.WriteLine(j.ToString());
                Console.WriteLine(
"TotalMemory:{0}", GC.GetTotalMemory(false
));
                Console.WriteLine(
"MaxGeneration:{0}"
, GC.MaxGeneration);
                Console.WriteLine(
"Value:{0},String:{1}"
, obj.Value, obj.String.Length);
                Console.WriteLine(
"Generation:{0}"
, generation);
                Console.WriteLine();
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            Console.Read();
        }


        
class TestObject
        
{
            
public int Value = 0
;
            
public string String = "0"
;

            
public
 TestObject()
            
{
                
for (int j = 0; j < 100; j++
)
                
{
                    Value
++
;
                    String 
+=
 j.ToString();
                }

            }

        }

    }

}

运行一个,结果如下:

    GC回收内存从0代开始,打扫0代中所有可以清除的对象。暂时不可清除的对象移到1代中。依此类推,清除1代对象时,尚用对象则移至2代。第一次回收之后,可回收内存空间已经很小,回收效果已不明显。故平常强制垃圾回收用函数GC.Collect()不如用GC.Collect(0)。
在AS3中,有垃圾自动回收机制,但是没有提供接口给用户,是不可操控的。但可以通过抛出某些对象的异常,来激发垃圾回收运行。代码如下:
public   class  GC
{
      
private function GC(){};

      
public static function Collect():void
      
{
            
try{
                  
new LocalConnection .connect("GC1");
                  
new LocalConnection .connect("GC2");
            }
catch(e:*){}
       }

}

对于比较耗费资源的对象,如LocalConnection,如果它们抛出异常,一般垃圾回收器不会坐视不理。那么,这个不怎么正宗的方法在.Net也可以吗?答案是肯定的。在.Net中,如果文件句柄、数据库连接等对象操作出错时,GC会尝试强制回收内存。修改上文Main函数代码如下,以作测试:
static   void  Main( string [] args)
        
{
            TestObject obj 
= new TestObject();
            
int generation = 0;

            generation 
= GC.GetGeneration(obj);
            Console.WriteLine(
0);
            Console.WriteLine(
"TotalMemory:{0}", GC.GetTotalMemory(false));
            Console.WriteLine(
"MaxGeneration:{0}", GC.MaxGeneration);
            Console.WriteLine(
"Value:{0},String:{1}", obj.Value, obj.String.Length);
            Console.WriteLine(
"Generation:{0}", generation);
            Console.WriteLine();

            
try
            
{
                
new SqlConnection("Null").Open();
            }

            
catch (Exception e) { }

            
for (int j = 1; j < 6; j++)
            
{
                generation 
= GC.GetGeneration(obj);
                Console.WriteLine(j.ToString());
                Console.WriteLine(
"TotalMemory:{0}", GC.GetTotalMemory(false));
                Console.WriteLine(
"MaxGeneration:{0}", GC.MaxGeneration);
                Console.WriteLine(
"Value:{0},String:{1}", obj.Value, obj.String.Length);
                Console.WriteLine(
"Generation:{0}", generation);
                Console.WriteLine();
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            Console.Read();
        }


运行一下,结果如图所示:
    可见,SqlConnection抛出异常时,GC果真进行了回收。再运行一下,结果却变了:

唏!怎么没有回收,内存反而升高了。可见,GC确实有点智能,第一次回收了,第二次似乎做了点别的动作,致使内存反而升高。Msdn2中有云,GC自己可以确定回收垃圾的最好时机与方法,所以奉劝用户一般不要手动干预,不然可能会南辕北辙。
       那.Net程序员在编程时应该怎么做,有没有一种既简单又有有效的方法来处理内在回收。愚人作以下建议,望各路高手不吝赐教:
       1,对于不包涵或没有引用(直接或间接)非托管资源的类,特别是作用如同Struct的实体类,析构、终结器、Dispose均不采用。
       2,对于包涵非托管资源的类,如数据库连接对象,文件句柄等,应继承IDispose接口,在Dispose方法中清理非托管对象。客户代码用using(…){}格式显示调用Dispose。如果继承了IDispose接口,Dispose方法就不要留空,这样没有任何意义。除了构造器,任何方法体留空都有害无益。
       3,所有自定义类一般均不建议显式声明析构函数、Finalize方法。 
实际上,.net CLR对垃圾回收采用的是代龄(generation)机制,也就是GC.Collect()或者GC.WaitForPendingFinalizers()并不一定会立即触发完整的垃圾回收操作的(尤其是对代龄>1的)。而且,使用WaitForPendingFinalizers会挂起线程,在服务程序中使用存在风险。
这问题得看具体情况,一般来说:
1、如果用到非托管资源,那么采用Dispose模式;
2、不要使用长弱引用,比如IList成员变量,一旦对象代龄增加到2,那基本上它就赖着了;
3、尽量不要使用GC.Collect(),CLR有自己的算法来处理,它未必听你的;
4、如果在线程中使用GC.Collect()或者导致了垃圾收集的执行,那么其他线程不能访问任何对象,CLR会挂起所有已经执行到临界区的线程,然后进行安全点识别,同时进行对象搬迁,呵呵,麻烦死人的,会很耗时,尽量避免。
还有,好像Debug版本的Assembly会阻止Garbage Collect,改用Release发布。
转贴:http://blog.csdn.net/luyifeiniu/archive/2007/12/26/1968802.aspx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值