.NET中的内存管理,GC机制,内存释放过程
引言
作为一个.NET程序员,我们知道托管代码的内存管理是自动的。.NET可以保证我们的托管程序在结束时全部释放,这为我们编程人员省去了不少麻烦,我们可以连想都不想怎么去管理内存,反正.NET自己会保证一切。好吧,有道理,有一定的道理。问题是,当我们用到非托管资源时.NET就不能自动管理了。这是因为非托管代码不受CLR(Common Language Runtime)控制,超出CLR的管理范围。那么如何处理这些非托管资源呢,.NET又是如何管理并释放托管资源的呢?
自动内存管理和GC
在原始程序中堆的内存分配是这样的:找到第一个有足够空间的内存地址(没被占用的),然后将该内存分配。当程序不再需要此内存中的信息时程序员需要手动将此内存释放。堆的内存是公用的,也就是说所有进程都有可能覆盖另一进程的内存内容,这就是为什么很多设计不当的程序甚至会让操作系统本身都down掉。我们有时碰到的程序莫名其妙的死掉了(随机现象),也是因为内存管理不当引起的(可能由于本身程序的内存问题或是外来程序造成的)。另一个常见的实例就是大家经常看到的游戏的Trainer,他们通过直接修改游戏的内存达到"无敌"的效果。明白了这些我们可以想象如果内存地址被用混乱了的话会多么危险,我们也可以想象为什么C++程序员(某些)一提起指针就头疼的原因了。另外,如果程序中的内存不被程序员手动释放的话那么这个内存就不会被重新分配,直到电脑重起为止,也就是我们所说的内存泄漏。所说的这些是在非托管代码中,CLR通过AppDomain实现代码间的隔离避免了这些内存管理问题,也就是说一个AppDomain在一般情况下不能读/写另一AppDomain的内存。托管内存释放就由GC(Garbage Collector)来负责。我们要进一步讲述的就是这个GC,但是在这之前要先讲一下托管代码中内存的分配,托管堆中内存的分配是顺序的,也就是说一个挨着一个的分配。这样内存分配的速度就要比原始程序高,但是高出的速度会被GC找回去。为什么?看过GC的工作方式后你就会知道答案了。
GC工作方式
首先我们要知道托管代码中的对象什么时候回收我们管不了(除非用GC.Collect强迫GC回收,这不推荐,后面会说明为什么)。GC会在它"高兴"的时候执行一次回收(这有许多原因,比如内存不够用时。这样做是为了提高内存分配、回收的效率)。那么如果我们用Destructor呢?同样不行,因为.NET中Destructor的概念已经不存在了,它变成了Finalizer,这会在后面讲到。目前请记住一个对象只有在没有任何引用的情况下才能够被回收。为了说明这一点请看下面这一段代码:
[C#]
object objA = new object();
object objB = objA;
objA = null;
// 强迫回收。
GC.Collect();
objB.ToString();
[Visual Basic]
Dim objA As New Object()
Dim objB As Object = objA
objA = Nothing
' 强迫回收。
GC.Collect()
objB.ToString()
这里objA引用的对象并没有被回收,因为这个对象还有另一个引用,ObjB。
对象在没有任何引用后就有条件被回收了。当GC回收时,它会做以下几步:
确定对象没有任何引用。
检查对象是否在Finalizer表上有记录。
如果在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的原因(除非你也像我一样,写一些有关GC的示例!^_^)。
Destructor的没落,Finalizer的诞生
对于Visual Basic程序员来说这是个新概念,所以前一部分讲述将着重对C++程序员。我们知道在C++中当对象被删除时(delete),Destructor中的代码会马上执行来做一些内存释放工作(或其他)。不过在.NET中由于GC的特殊工作方式,Destructor并不实际存在,事实上,当我们用Destructor的语法时,编译器会自动将它写为protected virtual void Finalize(),这个方法就是我所说的Finalizer。就象它的名字所说,它用来结束某些事物,不是用来摧毁(Destruct)事物。在Visual Basic中它就是以Finalize方法的形式出现的,所以Visual Basic程序员就不用操心了。C#程序员得用Destructor的语法写Finalizer,不过千万不要弄混了,.NET中已经没有Destructor了。C++中我们可以准确的知道什么时候会执行Destructor,不过在.NET中我们不能知道什么时候会执行Finalizer,因为它是在第一次对象回收操作后才执行的。我们也不能知道Finalizer的执行顺序,也就是说同样的情况下,A的Finalize可能先被执行,B的后执行,也可能A的后执行而B的先执行。也就是说,在Finalizer中我们的代码不能有任何的时间逻辑。下面我们以计算一个类有多少个实例为示例,指出Finalizer与Destructor的不同并指出在Finalizer中有时间逻辑的错误,因为Visual Basic中没有过Destructor所以示例只有C#版:
[C#]
public class CountObject {
public static int Count = 0;
public CountObject() {
Count++;
}
~CountObject() {
Count--;
}
}
static void Main() {
CountObject obj;
for (int i = 0; i < 5; i++) {
obj = null; // 这一步多余,这么写只是为了更清晰些!
obj = new CountObject();
}
// Count不会是1,因为Finalizer不会马上被触发,要等到有一次回收操作后才会被触发。
Console.WriteLine(CountObject.Count);
Console.ReadLine();
}
注意以上代码要是改用C++写的话会发生内存泄漏,因为我们没有用delete操作符手动清理内存,但是在托管代码中却不会发生内存泄漏,因为GC会自动检测没有引用了的对象并回收。这里作者推荐你只在实现IDisposable接口时配合使用Finalizer,在其他的情况下不要使用(可能会有特殊情况)。在非托管资源的释放一章我们会更好的了解IDisposable接口,现在让我们来做耶稣吧!
对象的复活
什么?回收的对象也可以"复活"吗?没错,虽然这么说的定义不准确。让我们先来看一段代码:
[C#]
public class Resurrection {
public int Data;
public Resurrection(int data) {
this.Data = data;
}
~Resurrection() {
Main.Instance = this;
}
}
public class Main {
public static Resurrection Instance;
public static void Main() {
Instance = new Resurrection(1);
Instance = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// 看到了吗,在这里“复活”了。
Console.WriteLine(Instance.Data);
Instance = null;
GC.Collect();
Console.ReadLine();
}
}
[Visual Basic]
Public Class Resurrection
Public Data As Integer
Public Sub New(ByVal data As Integer)
Me.Data = data
End Sub
Protected Overrides Sub Finalize()
Main.Instance = Me
MyBase.Finalize()
End Sub
End Class
Public Class Main
Public Shared Instance As Resurrection
Sub Main()
Instance = New Resurrection(1)
Instance = Nothing
GC.Collect()
GC.WaitForPendingFinalizers()
' 看到了吗,在这里“复活”了。
Console.WriteLine(Instance.Data)
Instance = Nothing
GC.Collect()
Console.ReadLine()
End Sub
End Class
你可能会问:"既然这个对象能复活,那么这个对象在程序结束后会被回收吗?"。会,"为什么?"。让我们按照GC的工作方式走一遍你就明白是怎么回事了。
1、执行Collect。检查引用。没问题,对象已经没有引用了。
2、创建新实例时已经在Finalizer表上作了纪录,所以我们检查到了对象有Finalizer。
3、因为查到了Finalizer,所以将记录移到Finalizer2表上。
4、在Finalizer2表上有记录,所以不释放内存。
5、Collect执行完毕。这时我们用了GC.WaitForPendingFinalizers,所以我们将等待所有Finalizer2表上的Finalizers的执行。
6、Finalizer执行后我们的Instance就又引用了我们的对象。(复活了)
7、再一次去除所有的引用。
8、执行Collect。检查引用。没问题。
9、由于上次已经将记录从Finalizer表删除,所以这次没有查到对象有Finalizer。
10、在Finalizer2表上也不存在,所以对象的内存被释放了。
现在你明白原因了,让我来告诉你"复活"的用处。嗯,这个……好吧,我不知道。其实,复活没有什么用处,而且这样做也非常的危险。看来这只能说是GC机制的漏洞(请参看GC.ReRegisterForFinalize再动脑筋想一下就知道为什么可以说是漏洞了)。作者建议大家忘掉有什么复活,避免这类的使用。可能你会问:"那你干吗还要对我们说这些?"我说这些为的是让大家更好的了解GC的工作机制!^_^
非托管资源的释放
到现在为止,我们说了托管内存的管理,那么当我们利用如数据库、文件等非托管资源时呢?这时我们就要用到.NET Framework中的标准:IDisposable接口。按照标准,所有有需要手动释放非托管资源的类都得实现此接口。这个接口只有一个方法,Dispose(),不过有相对的Guidelines指示如何实现此接口,在这里我向大家说一说。实现IDisposable这个接口的类需要有这样的结构:
[C#]
public class Base : IDisposable {
public void Dispose() {
this.Dispose(true);
GC.SupressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
// 托管类
}
// 非托管资源释放
}
~Base() {
this.Dispose(false);
}
}
public class Derive : Base {
protected override void Dispose(bool disposing) {
if (disposing) {
// 托管类
}
// 非托管资源释放
base.Dispose(disposing);
}
}
[Visual Basic]
Public Class Base
Implements IDisposable
Public Overloads Sub Dispose() Implements IDisposable.Dispose
Me.Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overloads Overridable Sub Dispose(ByVal disposing As Boolean)
If disposing Then
' 托管类
End If
' 非托管资源释放
End Sub
Protected Overrides Sub Finalize()
Me.Dispose(False)
MyBase.Finalize()
End Sub
End Class
Public Class Derive
Inherits Base
Protected Overloads Ov