.NET中GC.Collect该在什么时候调用?
最近做的一个项目,是从VB升级到VB.NET的代码,所以,数据库操作使用的是相对较老的方式OO4O (Oracle Objects for OLE)。其中,在较大的一个外部循环中调用一个函数,在该函数中执行了一个SQL查询,但是该外部循环执行到二百多次的时候出现了“ORA-01000:超出最多允许打开的游标数”的错误。查看代码,打开的RecordSet也已经关闭了,不知道问题出在什么地方。最后,在关闭RecordSet的地方添加了GC.Collect调用,问题解决。
示意代码如下:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Dim clsSession As Object 'Session Dim clsDatabase As Object 'Database Dim strSQL As String Dim lngCount As Long Dim rs As Object '记录集 clsSession = CreateObject("OracleInProcServer.XOraSession") clsDatabase = clsSession.OpenDatabase("HOST", "User/password", 0&) strSQL = "Select * FROM T_STA " ' 在一个循环中多次执行一个查询SQL For lngCount = 1 To 500 '执行SQL检索 rs = clsDatabase.CreateDynaset(strSQL, &H0&) Console.WriteLine(rs.RecordCount) rs.Close() ' 关闭记录集 rs = Nothing GC.Collect() '马上执行垃圾回收,如果注释掉该语句将出现CORA-01000的错误。 Next MessageBox.Show("OK") End Sub
该问题是解决了,但这引起了我对GC.Collect该如何使用的思考。
MSDN上说对该函数的频繁调用会引起垃圾回收的效率下降从而影响应用程序的性能。
第 0 代和第 1 代的垃圾回收的操作速度相对较快,需要的系统资源较少。而第 2 代的垃圾收集操作则与之相反,其活动时间较长,因而在垃圾回收器检查每个堆对象时,都会引起应用程序进程暂停。垃圾回收机制十分有效,因为它遵循系统中的内存分配方法并相应地调整自身的行为。尝试通过调用Collect或WaitForPendingFinalizers方法强制进行垃圾回收操作将干扰垃圾回收算法,通常会恶化应用程序的内存使用。
仅当您知道应用程序已用完只分配一次的大量内存时,才应调用Collect方法。例如,当应用程序已用完大缓存后,或者当大对话框关闭后而且您知道(或者有可能)将重新打开该对话框时,可调用Collect方法。
以前遇到过的使用GC.Collect 进行内存回收的情况:
1、使用Microsoft.Office.Interop.Excel.ApplicationClass操作Excel后。
2、使用WebBrowser后。
综合这次遇到的情况,可以总结出,在使用了非纯托管的组件后,要马上回收内存应该调用GC.Collect。分析原因应该是,在.NET中使用非托管的组件的时候,.NET自动进行了一个COM到.NET的互操作封装,在这个封装中是在Finalizer方法(该方法对应C++中的析构函数,.NET推荐的是实现IDispose接口的方式)中释放内存,而Finalizer这种内存释放方式相对低效。
实现了Finalizer函数的类型被回收的流程如下:
当GC回收时,它会做以下几步:
1、确定对象没有任何引用。
2、检查对象是否在Finalizer表上有记录。
3、如果在Finalizer表上有记录,那么将记录移到另外的一张表上,在这里我们叫它Finalizer2。如果不在Finalizer2表上有记录,那么释放内存。
在Finalizer2表上的对象的Finalizer会在另外一个low priority的线程上执行后从表上删除。当对象被创建时GC会检查对象是否有Finalizer,如果有就会在Finalizer表中添加纪录。我们这里所说的记录其实就是指针。如果仔细看这几个步骤,我们就会发现有Finalizer的对象第一次不会被回收,也就是,有Finalizer的对象要一次以上的Collect操作才会被回收,这样就要慢一步,所以作者推荐除非是绝对需要不要创建Finalizer。为了证明GC确实这么工作而不是作者胡说,我们将在对象的复活一章中给出一个示例,眼见为实,耳听为虚嘛!^_^GC为了提高回收的效率使用了Generation的概念,原理是这样的,第一次回收之前创建的对象属于Generation 0,之后,每次回收时这个Generation的号码就会向后挪一,也就是说,第二次回收时原来的Generation 0变成了Generation 1,而在第一次回收后和第二次回收前创建的对象将属于Generation 0。GC会先试着在属于Generation 0的对象中回收,因为这些是最新的,所以最有可能会被回收,比如一些函数中的局部变量在退出函数时就没有引用了(可被回收)。如果在Generation 0中回收了足够的内存,那么GC就不会再接着回收了,如果回收的还不够,那么GC就试着在Generation 1中回收,如果还不够就在Generation 2中回收,以此类推。Generation也有个最大限制,根据Framework版本而定,可以用GC.MaxGeneration获得。在回收了内存之后GC会重新排整内存,让数据间没有空格,这样是因为CLR顺序分配内存,所以内存之间不能有空着的内存。现在我们知道每次回收时都会浪费一定的CPU时间,这就是我说的一般不要手动GC.Collect的原因。