倒垃圾的艺术

本文对比了C++、Visual Basic 6和.NET Framework的垃圾回收机制。介绍了.NET中GC的工作原理,指出其比VB6引用计数机制更优。还阐述了Finalize()和Dispose()方法处理非托管资源的方式,给出了优化的垃圾回收方案,强调这是一门“倒垃圾的艺术”。

有人说,程序就是生活。其实这句话真的不为过。在生活中,充斥着物品的创造-〉使用-〉弃置的历程;在程序的代码里,也充斥着对象的实例化-〉使用-〉销毁的过程。不是这样的吗?

程序里的倒垃圾和生活中的倒垃圾本质上也是出于同样的动机:每个对象都占用着一定空间的内存,当一个对象使用完毕后,如果还让它霸占着内存就不像话了。在C++里,所有创建的对象都要由你自己动手去销毁它,如果你不严格地这么做,就会造成内存的泄漏。由程序员自己倒垃圾可以让程序运行的效率大大提高,但却让程序员不堪重负。因此,大多数新一代的面向对象语言都采用了生存期管理(lifetime management)技术,也就是由系统来判断对象何时不会再被使用的技术。在这篇文章里,我们一起来了解一下Visual Basic .NET里的生存期管理技术。说的庸俗一点,也就是倒垃圾的艺术

在讨论.NET里的垃圾回收机制之前,让我们一起先回顾一下在Visual Basic 6时代的垃圾回收技术。在那个COM的年代,当一个对象被创建后,它就会自行地计算有多少的客户端对它进行了引用。一旦它绝望地发现已经没有人在引用它自己时,它就义无反顾地自尽了,它占用的内存自然也归还到了内存堆上。这种垃圾回收机制叫做引用计数(reference counting)。

但是在.NET Framework下的生存期管理机制就很不一样了。CLR中,内存管理器(其实就是大名鼎鼎的垃圾回收器Garbage Collector,下文我们就直接用GC来称呼它!)掌握着一个指向空闲内存起始地址的指针。当需要创建对象申请空间时,将传回一个指针的副本,并根据需要内存的大小将指针指向下一个位置。只要内存堆还没用完,占用的内存就不会被回收,直到程序运行结束为止。嘿嘿,这种分配内存的方式真是再高效不过了!理论上说,C++的效率也不过于此。

不过内存总是会用完的,如果此时再有申请内存的需求,GC就要忙着先寻找可回收的资源了。GC的工作原理是个很深奥的话题,涕淌只想用很简洁的话来概括它。GC其实是.NET Framework下的一个级别很低的线程,它能够检查包含对象引用的变量(很直观嘛——如果一个对象可以通过程序的变量获得,那么这个对象就一定正在被使用)并把它们维护在一个结束队列(finalization queue)中;此外,GC还需要知道在这些对象中存不存在引用其他对象的成员(也就是核实有无循环引用)。通过以上方法,GC可以确定所有正在使用的内存,并把它们合并到堆的起始处的一个相邻块上,达到了回收资源的目的。

你可能要问我了,这种垃圾回收机制和VB6的相比有什么进步吗?嗯,首先,引用计数机制无条件地需要更多的系统开销;其次,引用计数比起GC容易引发内存的泄漏;再次,GC对循环引用的处理比引用计数简洁得多。

明白了上面的道理,我们就可以再扯得远一点,来看看这两个很典型的方法吧:Finalize()Dispose()。倒垃圾的艺术,就集中于此了。

Finalize()
字面的意思是结束。它是.NET Framework提供的回收非托管资源的方法(托管的资源是由GC自动回收的)。非托管资源,简单地说,就是除了内存之外的任何资源,比如数据库连接、窗体句柄、文件流等等的。System.Object类型就包含了Finalize()这个方法,不过里面虾米代志也不做。VB.NET允许在派生类中覆盖Finalize()(但是托管C++和C#原则上是不可以覆盖的!)。GC在实例被回收时会在这个类的实例上调用它。下面是一个很简单的例子:
Public Class MyClass
    Protected Overrides Sub Finalize()
        '注意了:VB.NET和C++的析构函数机制略有不同
        'VB.NET必须要显式调用基类的Finalize()方法
        MyBase.Finalize()
    End Sub
End Class

派生来的Finalize()方法必须要调用基类的Finalize()方法,这样才能释放基类代码分配的任何资源。当然如果这个类是由Object类直接派生来的,那倒是可以忽略(Object类的Finalize()方法啥都不干)——但是,还是养成习惯吧!多写这一句能有什么坏处呢?
         
其实Finalize()就是VB.NET中的析构函数,不过和C++的析构函数还是有一点点区别的。C++析构函数能够自动调用其基类的析构函数,但是VB.NET却不可以!所以,这就是涕淌向你婆婆妈妈地强调MyBase.Finalize()这句话的原因了。

垃圾回收机制和Finalize()方法貌似已经足够胜任所有的工作了,但是它们还是有缺陷的。因为垃圾回收系统中的对象是在垃圾回收出现或应用程序终止时才被销毁,这样的机制也叫做非确定性结束(VB6的引用计数虽然效率低些,但却是确定性结束)。非确定性结束会带来一些隐性的问题:比如一个具有稀缺资源(scarce resources)的对象(数据库连接就是个典型),如果没有在不需要时立即释放资源,程序可能会在内存耗尽之前就耗尽这些资源了。所以,我们需要一个方案,能够替代析构函数在涉及非托管资源时进行的非确定性结束。

Dispose()
幸运的是,总有那么一些聪明的人们,从C++中得到启发,编写了IDisposable接口,提供给持有非托管资源的对象实现。如果你的类实现了这个接口,你必须且仅必须实现其中的一个方法:Dispose()。Dispose字面意思是处置,这个方法没有参数,也没有返回值。这个方法CLR从不过问,只能由该对象的客户来调用。调用Dispose()方法便意味着这个对象已经不再被需要了,它必须无条件地立即释放其占有的所有非托管资源(事实上也释放了托管资源)。

类的编写者必须实现IDisposable.Dispose接口,而类的客户必须非常清楚什么时候调用它。但是难保客户不会忽略调用,所以类的编写者必须做好两手准备——也就是,仍然要提供类的Finalize()方法,在这个方法的内部调用Dispose()方法。比如下面就是一个比较简单的Finalize/Dispose垃圾回收方案:
Public Sub Dispose() Implements IDisposable.Dispose
    '添加释放资源的代码
End Sub

Protected Overrides Sub Finalize()
    '多调用一次并不会带来任何副作用,但是呃……
    Dispose()
    MyBase.Finalize()
End Sub

但是上面的代码有一个隐患涕淌刚才回避了,因为对象销毁的次序是不确定的。如果对象具有对其他对象的引用(属于托管资源),并且GC可能已经回收了那些对象,那么试图在已回收的对象上调用Dispose()方法将会引发ObjectDisposedException异常。

所以有必要把托管资源的处理单独地分离出来。这里有一个略为改进的版本,来看看吧:
Public Sub Dispose() Implements IDisposable.Dispose
    DisposeManagedResources()
    DisposeUnmanagedResources()
End Sub

Protected Overrides Sub Finalize()
    'Finalize()只释放了非托管资源
    '所以不必担心对附属对象调用Dispose()

    DisposeUnmanagedResources()
    MyBase.Finalize()
End Sub

Private Sub DisposeManagedResources()
    '添加释放附属对象的代码
End Sub

Private Sub DisposeUnmanagedResources()
    '添加释放非托管资源的代码
End Sub

客户调用了Dispose()方法,但是GC对此却一无所知。已被调用Dispose()方法的对象仍然存在于GC维护的结束队列里,也就是说,这个对象被客户销毁后,GC还要傻乎乎地重复一次该对象的析构。不但不起任何作用,还增加了系统的开销。所以,我们应该再把Dispose方法优化那么一下下:
Public Sub Dispose() Implements IDisposable.Dispose
    DisposeManagedResources()
    DisposeUnmanagedResources()
    'SuppressFinalize()乃GC类里的一个共享方法
    '它的字面意思是“强行结束”
    '它能够强制GC把某个对象从GC的结束队列中剔除
    '这样,便避免了对对象的重复析构

    GC.SuppressFinalize(Me)
End Sub

不知涕淌说了这么多,大家都明白了吗?为了能让网友们更好地理解,我跳过了太多太多的细节(也许也跳过了一些很重要的我自己无法意识到)。现在,涕淌给出一个相对完整版本的垃圾回收方法,留给你参考!

Public Class MyClass
    Implements IDisposable

    '这个变量标记对象是否已经被处置
    Private isDisposed As Boolean = False
    '在下面继续添加你需要的全局变量
    '...


    '类的构造函数,你可以完善它
    Public Sub New(...)
        '...
    End Sub

    '这个方法留给这个类的客户调用
    '释放所有的托管与非托管资源
    Public Sub Dispose() Implements IDisposable.Dispose
        If Not isDisposed Then
            DisposeManagedResources()
            DisposeUnmanagedResources()
            GC.SuppressFinalize(Me)
            isDisposed = True
        End If
    End Sub
   

    '如果客户忘了调用Dispose()方法
    '则由GC调用Finalize()方法来释放非托管资源
    Protected Overrides Sub Finalize()
        DisposeUnmanagedResources()
        MyBase.Finalize()
    End Sub

    Private Sub DisposeManagedResources()
        '添加释放附属对象的代码
    End Sub

    Private Sub DisposeUnmanagedResources()
        '添加释放非托管资源的代码
    End Sub

    Private Sub DoSomething()
        '当要访问对象的资源时
        '需要确认是否对象已被处置

        AssertNotDisposed()
    End Sub

    Private Sub AssertNotDisposed()
        '如果对象已被处置,必须抛出一个异常
        If isDisposed Then Throw _
            New ObjectDisposedException(Me.GetType().Name)
    End Sub

End Class

怎么样?倒垃圾还真是门艺术吧!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值