对于.Net CLR的垃圾自动回收,这两日有兴致小小研究了一下。查阅资料,写代码测试,发现不研究还罢,越研究越不明白了。在这里sban写下自己的心得以抛砖引玉,望各路高手多多指教。
近日浏览Msdn2,有一段很是费解,引于此处:原文:
英文:把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); 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(); } 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../t/t"); } static void TestOne() { new Beijing(); GC.Collect(); GC.WaitForPendingFinalizers(); Log.Write("In TestOne../t/t"); } }}
F5执行一下,返回如下结果:
Beijing's destructor is called TotalMilliseconds:53009847.4384 TotalMemory:701044China's destructor is called TotalMilliseconds:53009857.4528 TotalMemory:717428World's destructor is called TotalMilliseconds:53009867.4672 TotalMemory:733812In TestOne.. TotalMilliseconds:53009877.4816 TotalMemory:758388In Main.. TotalMilliseconds:53009887.496 TotalMemory:783056Beijing's destructor is called TotalMilliseconds:53589020.248 TotalMemory:697744China's destructor is called TotalMilliseconds:53589030.2624 TotalMemory:714128World's destructor is called TotalMilliseconds:53589040.2768 TotalMemory:738704In TestOne.. TotalMilliseconds:53589050.2912 TotalMemory:763280In Main.. TotalMilliseconds:53589060.3056 TotalMemory:779664 注:重点看时间与内存变化,以下同。
看来msdn此言非虚
但是为什么内存没有真正被GC回收呢?World的终结器既已执行,其中fs.Dispose()与conn.Dispose()也得以成功执行,为什么就连微软鼓励使用的Dispose()也不好使了呢?是fs与conn对象不占内存,差别微乎其微吗?为了验证是与不是,把上文例码中的fs与conn的相关定义及初始化代码一并去掉。再运行一下:Beijing's destructor is called TotalMilliseconds:54514090.4336 TotalMemory:566124China's destructor is called TotalMilliseconds:54514100.448 TotalMemory:582508World's destructor is called TotalMilliseconds:54514110.4624 TotalMemory:598892In TestOne.. TotalMilliseconds:54514120.4768 TotalMemory:623468In Main.. TotalMilliseconds:54514130.4912 TotalMemory:639852Beijing's destructor is called TotalMilliseconds:56343741.3424 TotalMemory:563252China's destructor is called TotalMilliseconds:56343751.3568 TotalMemory:579636World's destructor is called TotalMilliseconds:56343761.3712 TotalMemory:596020In TestOne.. TotalMilliseconds:56343771.3856 TotalMemory:620596In 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../t/t");}
运行一下,GC貌似无用,bj及其父类的析构函数依然在Log.Write("In TestOne...")之后执行,有无WaitForPendingFinalizers()无甚差别。但如果只new一下对象,并不赋值于变量,code如下:
static void TestOne(){ new Beijing(); GC.Collect(); Log.Write("In TestOne../t/t");}
运行结果如下:
Beijing's destructor is called TotalMilliseconds:59773883.6448 TotalMemory:562036In TestOne.. TotalMilliseconds:59773883.6448 TotalMemory:586612China's destructor is called TotalMilliseconds:59773893.6592 TotalMemory:602996In Main.. TotalMilliseconds:59773893.6592 TotalMemory:619380World's destructor is called TotalMilliseconds:59773903.6736 TotalMemory:635764Beijing's destructor is called TotalMilliseconds:59775696.2512 TotalMemory:561080China's destructor is called TotalMilliseconds:59775706.2656 TotalMemory:577464In TestOne.. TotalMilliseconds:59775706.2656 TotalMemory:602040World's destructor is called TotalMilliseconds:59775716.28 TotalMemory:618424In Main..
在.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方法。
原文地址: