对于.Net CLR的垃圾自动回收,这两日有兴致小小研究了一下。查阅资料,写代码测试,发现不研究还罢,越研究越不明白了。在这里sban写下自己的心得以抛砖引玉,望各路高手多多指教。
近日浏览Msdn2,有一段很是费解,引于此处:
英文:把zh-cn替成en-us。此文档对应.net2.0,把VS.80替成VS.90可查看.net3.5最新文档。两者无甚差别,可见自.net1.1之后,垃圾回收机制没有改变。
据上文引用,关于GC的二次回收,sban作图一张,如下:
为了验证GC对含有终结器对象的两次回收机制,我写了一个例子测试,代码如下:
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: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
1,垃圾回收时,bj对象及其父类终结器没有执行,而在进入Main之后执行。如果WaitForPendingFinalizers()方法保证了终结器线程有足够时间处理所有事情,那么便是可能由于GC的二次回收机制造成。
2,bj及其父类终结器执行时,内存却不降反升?不知是何缘故。
且说第一个疑问,如果是GC的二次回收机制所致,那么我手动回收它三次,bj及其父类终结器必然应该执行。修改上述代码中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: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
其一,当退出当前应用程序域时。msdn2中有云:
其二,当对象不能再被访问时调用GC.Collect。如把上文中代码TestOne改成如下:
{
Log.Write("对象创建之前"t"t");
new Beijing();
Log.Write("回收之前"t"t");
GC.Collect();
GC.WaitForPendingFinalizers();
Log.Write("回收之后"t"t");
}
回收之前 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代码如下:
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{..}。如下所示:
//原析构函数的代码
}finally{
base.Finalize();
}
扯远了,看上文中的第二个疑问,bj及其父类终结器执行时,内存却不降反升,是何缘故?
开头引用中有这么一句:
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代码如下,一次不行,连收三次成不成?
{
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");
}
且看运行结果如下:
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
微软建议对于欲实行手工回收的类,让其实现IDispose接口,在Dispose方法内清理资源。客户代码用using(..){..}调用。为什么要用这种格式调用,有二:
一,退出using代码块时,由CLR支持自动触发对象的Dispose,不需手工调用。如果在其内再行调用,纯属多余。
二,using是一个局部代码块,出了此块,块内对象均可用访问,符合GC回收时触发终结器的条件。
有一个问题,用using可以立马对资源进行清理吗?由于GC的二次回收机制,恐怕不会。写个代码测试一下:
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: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:
{
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: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保证。如果在派生类中不存在析造函数,却重载了基类的终结器,如下:
对于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.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)。但是如果只用GC.Collect(0),GC的二次回收机制会不会失效?
在AS3中,有垃圾自动回收机制,但是没有提供接口给用户,是不可操控的。但可以通过抛出某些对象的异常,来激发垃圾回收运行。代码如下:
{
private function GC(){};
public static function Collect():void
{
try{
new LocalConnection .connect("GC1");
new LocalConnection .connect("GC2");
}catch(e:*){}
}
}
对于比较耗费资源的对象,如LocalConnection,如果它们抛出异常,一般垃圾回收器不会坐视不理。那么,这个不怎么正宗的方法在.Net也可以吗?答案是肯定的。在.Net中,如果文件句柄、数据库连接等对象操作出错时,GC会尝试强制回收内存。修改上文Main函数代码如下,以作测试:
{
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果真进行了回收。再运行一下,结果却变了:
第一次异常引发的回收不再回收成功,第一次手工回收才可以。这验证了垃圾二次回收机制。但是首次运行时,为什么就可以回收呢?
那.Net程序员在编程时应该怎么做,有没有一种既简单又有有效的方法来处理内存回收。愚人作以下建议,望各路高手不吝赐教:
1,对于不包涵或没有引用(直接或间接)非托管资源的类,特别是作用如同Struct的实体类,析构、终结器均不采用。也不要实现IDispose接口。
2,对于包涵非托管资源的类,如数据库连接对象,文件句柄等,应继承IDispose接口,并且在Dispose方法中清理非托管对象。客户代码用using关键字调用,并且对于非托管大内存文档,最好手工调用一下GC.Collect。
3,所有自定义类一般均不建议显式声明析构函数、Finalize方法。
4,对于包涵事件委托的类,宜实现IDispose接口,在其中手工清理委托。
这四条建议思来想去不是很妥。关于C#的垃圾回收,若深究了,再长一倍怕也说不明白。接下来,将分成几篇来说,这一篇只算个引子。
补:非托客资源有哪些?
最常见的一类非托管资源就是包装操作系统资源的对象,例如文件,窗口,数据库连接或网络连接等。具体包括ApplicationContext,Brush,Component, ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image, Matrix,OdbcDataReader,OleDBDataReader
,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等
作者:sban 2007/12/1首发于博客园
sban 2007/12/3修改