自动内存管理是公共语言运行时在 托管执行期间提供的服务之一。 公共语言运行时的垃圾回收器管理应用程序的内存分配和释放。 对于开发人员来说,这意味着在开发托管应用程序时,无需编写代码来执行内存管理任务。 自动内存管理可以消除常见问题,例如忘记释放对象并导致内存泄漏,或尝试访问已释放对象的内存。 本部分介绍垃圾回收器如何分配和释放内存。
分配内存
初始化新进程时,运行时将为进程保留一个连续的地址空间区域。 这个保留的地址空间被称为托管堆。 托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。 最初,该指针设置为指向托管堆的基址。 托管堆上包含了所有引用类型。 当应用程序创建第一个引用类型时,将为托管堆基址处的类型分配内存。 当应用程序创建下一个对象时,垃圾回收器会在紧跟第一个对象的地址空间中为其分配内存。 只要地址空间可用,垃圾回收器将继续以这种方式为新对象分配空间。
从托管堆中分配内存要比非托管内存分配速度快。 由于运行时通过向指针添加值为对象分配内存,因此它几乎与从堆栈分配内存的速度一样快。 此外,由于连续分配的新对象存储在托管堆中,因此应用程序可以快速访问这些对象。
释放内存
垃圾回收器的优化引擎根据要进行的分配确定执行回收的最佳时间。 当垃圾回收器执行回收时,它将释放应用程序不再使用的对象内存。 它通过检查应用程序的根来确定不再使用哪些对象。 每个应用程序都有一组根。 每个根要么引用托管堆上的对象,要么设为 null。 应用程序的根包括静态字段、线程堆栈上的局部变量和参数,以及 CPU 寄存器。 垃圾回收器可以访问实时 (JIT) 编译器和运行时维护的活动根节点列表。 使用此列表,它会检查应用程序的根,在此过程中会创建一个图形,其中包含可从根访问的所有对象。
图形中不存在的对象无法从应用程序的根目录访问。 垃圾回收器会考虑无法访问的对象垃圾,并释放为其分配的内存。 在回收中,垃圾回收器检查托管堆,查找无法访问对象所占据的地址空间块。 发现每个无法访问的对象时,它使用内存复制函数压缩内存中可访问的对象,释放分配给不可访问对象的地址空间块。 压缩可访问对象的内存后,垃圾回收器会进行必要的指针更正,以便应用程序的根指向其新位置中的对象。 它还将托管堆的指针放置在最后一个可访问对象之后。 请注意,仅当集合发现大量无法访问的对象时,才会压缩内存。 如果托管堆中的所有对象在集合中幸存下来,则无需内存压缩。
为了提高性能,运行时为单独的堆中的大型对象分配内存。 垃圾回收器会自动释放大型对象的内存。 但是,为了避免在内存中移动大型对象,不会压缩此内存。
代系和性能
为了优化垃圾回收器的性能,托管堆分为三代:0、1 和 2。 运行时的垃圾回收算法基于以下几个普遍原理,这些垃圾回收方案的原理已在计算机软件业通过实验得到了证实。 首先,压缩托管堆的一部分内存要比压缩整个托管堆速度快。 其次,较新的对象将具有较短的生存期,较旧的对象将具有更长的生存期。 最后,较新的对象往往彼此相关,并同时由应用程序访问。
运行时的垃圾回收器将新对象存储在第 0 代中。 在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第 1 级和第 2 级中。 本主题中稍后介绍了对象升级过程。 由于压缩托管堆的一部分比整个堆更快,因此此方案允许垃圾回收器在特定生成中释放内存,而不是每次执行回收时释放整个托管堆的内存。
实际上,当第0代内存已满时,垃圾回收器会执行回收。 如果应用程序在第 0 代满时尝试创建新对象,垃圾回收器发现第 0 代中没有为对象分配的地址空间。 垃圾回收器执行回收,尝试为对象释放第 0 级托管堆中的地址空间。 垃圾回收器首先检查第 0 代中的对象,而不是托管堆中的所有对象。 这是最有效的方法,因为新对象往往具有较短的生存期,预计执行集合时,应用程序将不再使用第 0 代中的许多对象。 此外,仅第 0 代的集合会回收足够的内存,使应用程序能够继续创建新对象。
垃圾回收器执行第 0 代回收后,它会压缩可访问对象的内存,如本主题前面的 “释放内存 ”中所述。 然后,垃圾回收器升级这些对象,并考虑第 1 级托管堆的这一部分。 由于在集合中幸存的对象往往具有更长的生存期,因此将其提升到更高的代系是有意义的。 因此,垃圾回收器不必每次执行第 0 代回收时重新检查第 1 代和第 2 代中的对象。
在垃圾回收器对第 0 代进行第一次回收,并将可访问对象提升至第 1 代后,它将剩下的托管堆部分继续视作第 0 代。 它继续为第 0 代中的新对象分配内存,直到第 0 代已满,并且需要执行另一个集合。 此时,垃圾回收器的优化引擎确定是否需要检查旧代中的对象。 例如,如果第 0 代集合未回收足够的内存,以便应用程序成功完成其创建新对象的尝试,则垃圾回收器可以执行第 1 代(第 2 代)的回收。 如果这无法回收足够的内存,垃圾回收器可以执行第 2 代、第 1 代和 0 代的回收。 每次回收后,垃圾回收器都会压缩第 0 代中的可访问对象,并将其提升为第 1 代。 第 1 代中幸存的集合中的对象被提升为第 2 代。 由于垃圾回收器仅支持三代,对于在回收过程中幸存的第 2 代对象,它们将保留在第 2 代中,直到在将来的回收过程中被判定为无法访问。
为非托管资源释放内存
对于应用程序创建的大多数对象,可以依赖垃圾回收器自动执行必要的内存管理任务。 但是,非托管资源需要显式清除。 最常见的非托管资源类型是封装操作系统资源的对象,例如文件句柄、窗口句柄或网络连接。 尽管垃圾回收器能够跟踪封装非托管资源的托管对象的生存期,但它没有有关如何清理资源的具体知识。 创建封装非托管资源的对象时,建议提供必要的代码来清理公共 Dispose 方法中的非托管资源。 通过提供 Dispose 方法,可以让用户在对象完成后显式释放其内存。 使用封装非托管资源的对象时,应注意 Dispose 并根据需要调用它。 有关清理非托管资源的详细信息以及实现 Dispose 的设计模式的示例,请参阅 垃圾回收。