• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

浅谈C#的垃圾回收-关于GC、析构函数、Dispose、andFinalize

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
转载:
     对于.Net CLR的垃圾自动回收,这两日有兴致小小研究了一下。查阅资料,写代码测试,发现不研究还罢,越研究越不明白了。在这里sban写下自己的心得以抛砖引玉,望各路高手多多指教。

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

    据上文引用,关于GC的二次回收,sban作图一张,如下:


    为了验证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);//当前里程休息一下,好使GC有机可乘
            using (StreamWriter sw = File.AppendText(logFilePath))
            //此处有可能抛出文件正在使用的异常,但不影响测试
            {
                sw.WriteLine(
"{0}"tTotalMilliseconds:{1}"tTotalMemory:{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();
        }

       
protectedvoid 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(
"进入Main之后"t"t");
        }

        
static void TestOne()
        {
            Log.Write("对象创建之前"t"t");
            Beijing bj = new Beijing();
            Log.Write("回收之前"t"t");
            GC.Collect();
            GC.WaitForPendingFinalizers();//此方法相当于join终结器线程,等待执行完毕。

            Log.Write("回收之后"t"t");
        }
    }
}

    F5执行一下,返回如下结果:
对象创建之前            TotalMilliseconds:4642180.4976            TotalMemory:624780
回收之前            TotalMilliseconds:4642220.5552                TotalMemory:895116
回收之后            TotalMilliseconds:4642230.5696                TotalMemory:611468
进入Main之后            TotalMilliseconds:4642240.584                TotalMemory:636044
Beijing's destructor is called    TotalMilliseconds:4642260.6128    TotalMemory:660620
China's destructor is called    TotalMilliseconds:4642270.6272    TotalMemory:677004
World's destructor is called    TotalMilliseconds:4642280.6416    TotalMemory:693932

对象创建之前            TotalMilliseconds:4643302.1104              TotalMemory:616588
回收之前            TotalMilliseconds:4643352.1824                 TotalMemory:886924
回收之后            TotalMilliseconds:4643362.1968                TotalMemory:611468
进入Main之后            TotalMilliseconds:4643372.2112               TotalMemory:636044
Beijing's destructor is called    TotalMilliseconds:4643392.24    TotalMemory:660620
China's destructor is called    TotalMilliseconds:4643402.2544    TotalMemory:677004
World's destructor is called    TotalMilliseconds:4643412.2688    TotalMemory:693932

结果一

    从结果看,垃圾回收卓有成效,两次平均下降了200000K左右。但有几点疑问:
    1,垃圾回收时,bj对象及其父类终结器没有执行,而在进入Main之后执行。如果WaitForPendingFinalizers()方法保证了终结器线程有足够时间处理所有事情,那么便是可能由于GC的二次回收机制造成。
    2,bj及其父类终结器执行时,内存却不降反升?不知是何缘故。

    且说第一个疑问,如果是GC的二次回收机制所致,那么我手动回收它三次,bj及其父类终结器必然应该执行。修改上述代码中
TestOne函数如下:
        static void TestOne()
        {
            Log.Write(
"对象创建之前"t"t");

            Beijing bj 
= new Beijing();

            Log.Write(
"第一次回收之前"t"t");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"第二次回收之前"t"t");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"第三次回收之前"t"t");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"回收之后"t"t");
        }

    运行,如果如下所示:
对象创建之前            TotalMilliseconds:5750704.4768    TotalMemory:616588
第一次回收之前            TotalMilliseconds:
5750744.5344    TotalMemory:886924
第二次回收之前            TotalMilliseconds:
5750754.5488    TotalMemory:611544
第三次回收之前            TotalMilliseconds:
5750764.5632            TotalMemory:609344
回收之后            TotalMilliseconds:
5750774.5776                TotalMemory:609344
In Main..            TotalMilliseconds:
5750784.592                TotalMemory:633920
Beijing
's destructor is called    TotalMilliseconds:5750804.6208    TotalMemory:658496
China's destructor is called    TotalMilliseconds:5750814.6352    TotalMemory:674880
World's destructor is called    TotalMilliseconds:5750824.6496    TotalMemory:691808

对象创建之前            TotalMilliseconds:
5751836.104            TotalMemory:616588
第一次回收之前            TotalMilliseconds:
5751876.1616         TotalMemory:886924
第二次回收之前            TotalMilliseconds:
5751886.176        TotalMemory:611544
第三次回收之前            TotalMilliseconds:
5751896.1904        TotalMemory:609344
回收之后            TotalMilliseconds:
5751906.2048            TotalMemory:609344
In Main..            TotalMilliseconds:
5751916.2192            TotalMemory:633920
Beijing
's destructor is called    TotalMilliseconds:5751936.248    TotalMemory:658496
China's destructor is called    TotalMilliseconds:5751946.2624    TotalMemory:674880
World's destructor is called    TotalMilliseconds:5751956.2768    TotalMemory:691808
    结果二

    从结果看,并不是GC的二次回收机制让bj及其父类终结器没有回收。在两种条件下,GC将触发对象的终结器:
    其一,当退出当前应用程序域时。msdn2中有云:
在应用程序域的关闭过程中,对没有免除终结的对象将自动调用Finalize,即使那些对象仍是可访问的。
上面便是这种情况。
    其二,当对象不能再被访问时调用GC.Collect。如把上文中代码TestOne改成如下:
        static void TestOne()
        {
            Log.Write(
"对象创建之前"t"t");

            
new Beijing();

            Log.Write(
"回收之前"t"t");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"回收之后"t"t");
        }
    其结果便为:
对象创建之前            TotalMilliseconds:6387650.36                TotalMemory:616588
回收之前            TotalMilliseconds:
6387700.432                TotalMemory:886924
Beijing
's destructor is called    TotalMilliseconds:6387710.4464    TotalMemory:603276
China's destructor is called    TotalMilliseconds:6387720.4608    TotalMemory:619660
World's destructor is called    TotalMilliseconds:6387730.4752    TotalMemory:644780
回收之后            TotalMilliseconds:6387740.4896            TotalMemory:669356
In Main..            TotalMilliseconds:
6387750.504            TotalMemory:685740

对象创建之前            TotalMilliseconds:
6388792.0016            TotalMemory:616588
回收之前            TotalMilliseconds:
6388832.0592                TotalMemory:886924
Beijing
's destructor is called    TotalMilliseconds:6388842.0736    TotalMemory:603184
China's destructor is called    TotalMilliseconds:6388852.088    TotalMemory:619568
World's destructor is called    TotalMilliseconds:6388862.1024    TotalMemory:644688
回收之后            TotalMilliseconds:6388872.1168                TotalMemory:669264
In Main..            TotalMilliseconds:
6388882.1312            TotalMemory:685648
结果三

    结果显示,bj对象及其父类的析构,在退出当前应用程序域前便已执行。bj是含有终结器的不能访问的对象,为什么GC在此没有信守二次回收的承诺?

    BTW,上文代码中Beijing和China中本没有声明终结器[Finalize],只有析构函数,为什么我统称为终结器?因为在C#中,本没有析构函数,析构也是终结器。看一看Beijing的IL截图:



   其中只有Finalize,不见.cctor。而Finalize的IL代码如下:
.method family hidebysig virtual instance void 
        Finalize() cil managed
{
  
// Code size       25 (0x19)
  .maxstack  1
  .
try
  {
    IL_0000:  nop
    IL_0001:  ldstr      
"Beijing's destructor is called"
    IL_0006:  call       
void Lab.Log::Write(string)
    IL_000b:  nop
    IL_000c:  nop
    IL_000d:  leave.s    IL_0017
  }  
// end .try
  finally
  {
    IL_000f:  ldarg.
0
    IL_0010:  call       instance void Lab.China::Finalize()

    IL_0015:  nop
    IL_0016:  endfinally
  }  
// end handler
  IL_0017:  nop
  IL_0018:  ret
// end of method Beijing::Finalize


    由此IL代码可见,析构函数在编译之后其实是不存在的。用户在析构体内敲出的代码,在编译时将被取出,放在一个新增的受保护的Finalize(名以终结器)之内,同时外围加一个try{...}finally{..}。如下所示:
try{
    //原析构函数的代码
}finally{
    base.Finalize();
}
    C#中的析构函数只是.Net语言架构师一个设计的小手段。

    扯远了,看上文中的第二个疑问,bj及其父类终结器执行时,内存却不降反升,是何缘故?
    开头引用中有这么一句:
实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。
    看来确实如此。World::Finalize中的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。让它回收,它确实没有给我干活啊。是不是因为GC的二次回收机制,一次GC.Collect并不足以回收。于是我修改上文中TestOne代码如下,一次不行,连收三次成不成?
        static void TestOne()
        {
            
new Beijing();

            Log.Write(
"第一次回收之前");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"第二次回收之前");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"第三次回收之前");
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Log.Write(
"In TestOne.."t"t");
        }

    且看运行结果如下:
第一次回收之前    TotalMilliseconds:2289517.5344                  TotalMemory:616588
Beijing
's destructor is called    TotalMilliseconds:2289527.5488    TotalMemory:467468
China's destructor is called    TotalMilliseconds:2289537.5632    TotalMemory:483852
World's destructor is called    TotalMilliseconds:2289547.5776    TotalMemory:500236
第二次回收之前    TotalMilliseconds:2289557.592                   TotalMemory:524812
第三次回收之前    TotalMilliseconds:2289567.6064            TotalMemory:474636
In TestOne..            TotalMilliseconds:
2289577.6208             TotalMemory:474648
In Main..            TotalMilliseconds:
2289587.6352                TotalMemory:499224

第一次回收之前    TotalMilliseconds:2290709.248                   TotalMemory:616588
Beijing
's destructor is called    TotalMilliseconds:2290719.2624    TotalMemory:467448
China's destructor is called    TotalMilliseconds:2290729.2768    TotalMemory:483832
World's destructor is called    TotalMilliseconds:2290739.2912    TotalMemory:504656
第二次回收之前    TotalMilliseconds:2290749.3056               TotalMemory:529232
第三次回收之前    TotalMilliseconds:2290759.32                 TotalMemory:474760
In TestOne..            TotalMilliseconds:
2290769.3344            TotalMemory:474772
In Main..            TotalMilliseconds:
2290779.3488                TotalMemory:499348
结果五
    结果显示,第二次回收之后,三次回收之前内存明显降低。看来开关一段引用中msdn2所言非虚

    微软建议对于欲实行手工回收的类,让其实现IDispose接口,在Dispose方法内清理资源。客户代码用using(..){..}调用。为什么要用这种格式调用,有二:
    一,退出using代码块时,由CLR支持自动触发对象的Dispose,不需手工调用。如果在其内再行调用,纯属多余。
    二,using是一个局部代码块,出了此块,块内对象均可用访问,符合GC回收时触发终结器的条件。

    有一个问题,用using可以立马对资源进行清理吗?由于GC的二次回收机制,恐怕不会。写个代码测试一下:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.IO;

namespace Lab
{
    
class Program
    {

        
static void Main(string[] args)
        {
            Log.Write(
"创建对象之前"t"t");

            
using (ClassOne c = new ClassOne())
            {
                Log.Write(
"创建对象之后"t"t");
            }

            Log.Write(
"退出程序之前"t"t");
        }

    }

    
class ClassOne : IDisposable
    {
        
protected FileStream fs = null;
        
protected SqlConnection conn = null;

        
public ClassOne()
        {
            fs 
= new FileStream(@"d:"temp.txt", FileMode.Open);
            conn 
= new SqlConnection();
        }

        
#region IDisposable Members

        
public void Dispose()
        {
            fs.Dispose();
            conn.Dispose();
        }

        
#endregion
    }
}
   
    运行如果如下:
创建对象之前            TotalMilliseconds:9367314.9072    TotalMemory:649356
创建对象之后            TotalMilliseconds:
9367354.9648    TotalMemory:911500
退出程序之前            TotalMilliseconds:
9367395.0224    TotalMemory:619908

创建对象之前            TotalMilliseconds:
9368576.7216    TotalMemory:616588
创建对象之后            TotalMilliseconds:
9368616.7792    TotalMemory:886924
退出程序之前            TotalMilliseconds:
9368626.7936    TotalMemory:911500

创建对象之前            TotalMilliseconds:
9369968.7232    TotalMemory:616588
创建对象之后            TotalMilliseconds:
9370008.7808    TotalMemory:886924
退出程序之前            TotalMilliseconds:
9370018.7952    TotalMemory:911500

    从结果看,第一次回收了,但此后,GC似乎有了记忆,显示没有回收。修改一下,加一个GC.Collect:
        static void Main(string[] args)
        {
            Log.Write(
"创建对象之前"t"t");

            
using (ClassOne c = new ClassOne())
            {
                Log.Write(
"创建对象之后"t"t");
                GC.Collect();
                GC.WaitForPendingFinalizers();
                Log.Write(
"回收之后"t"t");
            }

            Log.Write(
"退出程序之前"t"t");
        }
   
    运行一,结果还令人满意:
创建对象之前            TotalMilliseconds:9650221.7072    TotalMemory:616588
创建对象之后            TotalMilliseconds:
9650261.7648    TotalMemory:886924
回收之后            TotalMilliseconds:
9650271.7792    TotalMemory:611468
退出程序之前            TotalMilliseconds:
9650281.7936    TotalMemory:636044

创建对象之前            TotalMilliseconds:
9651383.3776    TotalMemory:616588
创建对象之后            TotalMilliseconds:
9651423.4352    TotalMemory:886924
回收之后            TotalMilliseconds:
9651433.4496    TotalMemory:611468
退出程序之前            TotalMilliseconds:
9651443.464    TotalMemory:636044

创建对象之前            TotalMilliseconds:
9652555.0624    TotalMemory:616588
创建对象之后            TotalMilliseconds:
9652645.192    TotalMemory:886924
回收之后            TotalMilliseconds:
9652655.2064    TotalMemory:611468
退出程序之前            TotalMilliseconds:
9652665.2208    TotalMemory:636044
    这便验证了GC的二次回收机制。

    在C#中,如果一个自定义类没有构造器,编译器会添加一个隐藏的无参构造器。但是析构函数不会自动创建。但是如果析构函数被创建了,终结器也便自动产生了,这一点可以通过同时定义析构函数与Finalize得出,编译器将报“Finalize方法名称重复”之错。Finalize与析构函数二者只容其一,这一点由csc保证。如果在派生类中不存在析造函数,却重载了基类的终结器,如下:
protected override void Finalize(){...}
    而这个方法又不显式调用base.Finalize(),那么GC将忽略基类。可以利用这个特性写一个不受垃圾回收器管辖的类,以实现某种特殊的效果。此乃旁边左道,与高手见笑了。
   
    对于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代对象时,尚

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
C#windowsmediaplayer详细用法(最全面)发布时间:2022-07-13
下一篇:
c#栈和队列习题发布时间:2022-07-13
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap