第二章 .NET 资源管理

By D.S.Qiu

尊重他人的劳动,支持原创,转载请注明出处:http://dsqiu.iteye.com

一个很简单的事实,.NET 程序运行在托管的环境有对创建有效 C# 的设计有很大影响。想要利用这个环境的优势需要你的思维从其他环境的转换到 .NET 公共语言运行库(CLR)上去。这意味着你需要理解 .NET 垃圾回收器。.NET 内存管理环境的概述对于理解本章的具体建议还是很有必要的,所以我们从这个概述开始。

垃圾回收器(GC)为你控制托管内存。不像自然环境,你不需要负责大多数的内存泄露,悬浮指针,未初始化指针,或者其他一些了内存管理问题。但是垃圾回收器没那么神奇:你也需要自己清理。你要负责非托管资源,例如文件句柄,数据库连接,GDI+对象,COM对象,和其他系统对象。此外你可能引起对象停留在内存比你想象的久因为你使用 event 处理或 delegate 创建它们之间的连接。查询,它执行等待结果,也会引起对象保持引用时间比你期望的更长。在闭包中捕获约束变量,并且这些约束变量会可以一直可访问直到已经离开了这个结果的作用域。

好消息是:因为 GC 控制内存,明确的设计会更容易实现。无论是循环引用,还是简单关系和复杂的 web 对象,都是更容易的。GC 的

标记和压缩算法可以高效检测到这些关系和完全移除不可达 web 对象。 GC 决定对象是否可达是通过对象树形结构从根部开始漫游,而不是强制跟踪每个对象的引用,COM 就是这样的。 EntitySet 类提供了例子,它的算法简化了对对象关系的判定。实体是从数据数据库加载的对象集合。每个实体可能包含其他实体对象的引用。这些实体还可以能包含对其他实体的链接。就像关系数据库的实体集模型,这些链接和引用都是可循环的。

有些引用是展示不同 EntitySet 的 web 对象。释放内存是 GC 的责任。因为 .NET 框架设计者不需要释放这些对象,web 对象引用的释放的复杂性不会构成问题。不需要决定 web 对象的合理释放次序,这是 GC 的工作。GC 的设计简化了识别这类 web 对象为垃圾的问题。应用可以停止引用任何实体就是垃圾。垃圾回收器会知道是否这个实体仍然由应用中活着的对象可到达。应用中任何不可达到的对象都是垃圾。

垃圾回收器运行自己的线程去移除程序中没有用的内存。它每次也会压缩托管的堆内存。压缩堆是通过移动活着的对象到一个托管的堆中以使得未使用的内存在一个连续的内存块。图2.1 就是垃圾回收前和后堆内存的截图对比。GC 处理完所有空闲内存都被放在连续的块中。

就像你刚学到的,内存管理(堆内存的管理)完全是垃圾回收器的责任。其他系统的资源必须有开发者管理:你和使用你的类的人。两种机制帮助开发者控制的非托管资源的寿命:析构函数和 IDisposable 接口。析构函数是一个被动的机制确保你的对象总是有方式释放非托管资源。析构函数会有很多缺点,所有你也可以实现 IDisposable 接口提供几乎不入侵的方式及时返回资源给系统。

析构函数被垃圾回收器调用。它们会在对象变成垃圾之后某个时间被调用。你不知道什么时候调用。你只能知道的是如果某个时间被调用了你的对象就不可到达。这是跟 C++ 很大的不同,并且对你的设计有很重要的影响。要经验的 C++ 程序员编写类总是在构造函数分配重要资源然后在析构函数释放:

// Good C++, bad C#:
class CriticalSection
{
    // Constructor acquires the system resource.
    public CriticalSection()
    {
        EnterCriticalSection();
    }
    // Destructor releases system resource.
    ~CriticalSection()
    {
        ExitCriticalSection();
    }
    private void ExitCriticalSection()
    {
        throw new NotImplementedException();
    }
    private void EnterCriticalSection()
    {
        throw new NotImplementedException();
    }
}

// usage:
void Func()
{
    // The lifetime of s controls access to
    // the system resource.
    CriticalSection s = new CriticalSection();
    // Do work.
    //...
    // compiler generates call to destructor.
    // code exits critical section.
}

通常的 C++ 习惯是保证资源回收是没有异常。这在 C# 是行不通的,至少,不是同样的方式。确定的析构函数不是 .NET 环境或 C# 语言的一部分。在 C# 语言尝试强制 C++ 习惯的确定的析构函数不会很好的奏效。在 C# ,析构函数最后才执行,但它不会及时执行。在上面的例子中,代码最后会退出临界区,但是在 C# 中,函数退出后它不会退出临界区。那会在后面确定的时间发生。你不可能知道什么时候。析构函数只是保证对象申请的非托管资源会最终释放。但析构函数执行没有确定的时间,所以你的设计和编码实践应该尽量减少析构函数的创建,同时也尽量减少析构函数的执行如果它存在的话。在本章中你将学习到什么时候你一定要创建析构函数,以及如何减少有析构函数的负面影响。

依赖于析构函数还会引入性能的损失。需要析构函数对象会被垃圾回收器消耗一部分性能。当 GC 发现对象是垃圾但是需要执行析构,它不能将对象理解从内存中移除。首先,它调用析构函数。析构函数不是在和垃圾回收器同一个线程中执行的。而是, GC 把每个等待析构的对象放进一个队列中并且起另一个线程去执行所有析构函数。它会持续进行,把垃圾移除。在下一次 GC 循环时,已经被析构的对象就会被移除内存。图2.2 显示3种 GC 操作和不同内存使用。注意到需要析构的对象会留着内存多些循环周期。

这可能会让你认为需要析构的对象会在内存比一般对象多待一个 GC 周期。我只是简化地描述。它是比我描述的更复杂因为 GC 的设计决策。.NET 垃圾回收器定义代来优化工作。代帮助 GC 更快确定最有可能是垃圾的候选对象。从上次垃圾回收操作创建的对象都是0代对象。在一次 GC 操作后存活下来的就是1代对象。经过2次或更多 GC 操作存活的是2代对象。代的目的是区分局部变量和生命周期是整个应用的对象。0代的对象大多数都是局部对象。成员变量和全家变量很快会进入1代而且最后进入2代。

GC 通过现在检查1代和2代对象的频率来优化工作。每次 GC 循环都会检查0代对象。粗略假设 GC 会10次检查0代和1代对象。而要超过100次检查所有对象。考虑析构以及它的开销:需要析构的对象要比不需要析构的对象多待超过9个回收循环。如果仍然没有被析构,它将进入2代。在2代,对象会生存上100个循环知道下次2代回收。我已经花了一些时间解释为什么析构函数不是一个好的解决方案。但是,你仍需要释放资源。解决这些问题你可以使用 IDisposable 接口和标准回收模式(查看本章原则17)。

在最后,记住托管环境垃圾回收器会负责内存管理,最大的好处是:内存泄露和其他指针相关的问题不再是你的问题。非内存资源你要强制创建析构函数来保证正确清理那些非内存资源。析构函数会比较大影响你程序的性能,但是你必须实现它以避免内存泄露。实现和使用 IDisposable 接口避免析构函数引入的垃圾回收的性能消耗。下一节将进入具体的原则,帮助您创建程序,更有效地利用环境。

小结:

终于翻译完第二章了,不断不断坚持,还是觉得要完成,本来凌晨2:00就要结束第二章的战斗的,后面状态还是没有调整过来,从昨天翻译有20页,虽然现在感觉印象还不是特别深刻,但是至少有用还是可以明确感受到。

附上第二章的目录:

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!

有关本书的其他章节翻译请点击查看,转载请注明出处,尊重原创!

如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件([email protected])交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

转载请在文首注明出处:http://dsqiu.iteye.com/blog/2079806

更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)