在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
对于.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{
C#中的析构函数只是.Net语言架构师一个设计的小手段。//原析构函数的代码 }finally{ base.Finalize(); } 扯远了,看上文中的第二个疑问,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
这便验证了GC的二次回收机制。创建对象之后 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 在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代对象时,尚 |
请发表评论