第 21 章 托管堆和垃圾回收
第 21 章 托管堆和垃圾回收 本章内容 托管堆基础 代:提升性能 使用需要特殊清理的类型 手动监视和控制对象生存期 本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存。简单地说,本章要解释 CLR 中的垃圾回收器是如何工作的,还要解释相关的性能问题。另外,本章讨论了如何设计应用程序来最有效地使用内存。 21.1 托管堆基础 每个程序都要使用这样或那样的资源,包括文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。以下是访问一个资源所需的步骤。 调用 IL 执行 newobj,为代表资源的类型分配内存(一般使用 C# new 操作符来完成)。 初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。 访问类型的成员来使用资源(有必要可以重复)。 摧毁资源的状态以进行清理。 释放内存。垃圾回收器独自负责这一步。 如果需要程序员手动管理内存(例如,原生 C++ 开发人员就是这样的),这个看似简单的模式就会成为导致大量编程错误的“元凶”之一。想想看,有多少次程序员忘记释放不再需要的内存而造成内存泄漏?又有多少次视图使用已经释放的内存,然后由于内存被破坏而造成程序错误和安全漏洞?而且,这两种bug比其他大多数 bug 都要严重,因为一般无法预测他们的后果或发生的时间①。如果是其他bug, 一旦发现程序行为异常,改正出问题的代码行就可以了。 ① 例如,访问越界的bug 可能取回不相干的数据,使程序结果变得不正确。而且错误没有规律,让人捉摸不定。 ————译注 现在,只要写的是可验证的、类型安全的代码(不要用 C# unsafe 关键字),应用程序就不可能会出现内存被破坏的情况。内存仍有可能泄露,但不像以前那样是默认行为。现在内存泄漏一般是因为在集合中存储了对象,但不需要对象的时候一直不去删除 为了进一步简化编程,开发人员经常使用的大多数类型都不需要步骤 4 (摧毁资源的状态以进行清理)。所以,托管堆除了能避免前面提到的 bug,还能为开发人员提供一个简化的编程模型;分配并初始化资源并直接使用。大多数类型都无需资源清理,垃圾回收器会自动释放内存。 使用需要特殊清理的类型时,编程模型还是像刚才描述的那样简单。只是有时需要尽快清理资源,而不是非要等着 GC ①介入。可在这些类中调用一个额外的方法(称为 Dispose),按照自己的节奏清理资源。另一方面,实现这样的类需要考虑到较多的问题(21.4 节会详细讨论)。一般只有包装了本机资源(文件、套接字和数据库连接等)的类型才需要特殊清理。 ① 垃圾回收、垃圾回收器都可以简称为 GC。 ———— 译注 21.1.1 从托管堆分配资源 CLR 要求所有对象都从托管堆分配。进程初始化时,CLR 划出一个地址空间区域作为托管堆。CLR 还要维护一个指针,我把它称作 NextObjPtr。该指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr 设为地址空间区域的基地址。 一个区域被废垃圾对象填满后,CLR 会分配更多的区域。这个过程一直重复,直至整个进程地址空间都被填满。所以,你的应用程序的内存受进程的虚拟地址空间的限制。32 为进程最多能分配 1.5 GB,64 位进程最多能分配 8 TB。...