自动内存管理是 CLR 在执行托管代码过程中提供的一项功能。CLR 的GC (Garbage Collection, 垃圾回收器, 下同)可以自动帮助程序分配或释放内存。对于开发者来说,这意味着开发者并不需要写额外的代码来处理内存管理的事情了,同时这也避免了比较常见的错误,比如:
- 忘记释放内存
- 释放已经释放过的内存
- 访问一个已经被释放的内存
本文将会讨论 GC 在分配和释放内存时是如何工作的。
分配内存
当建立一个新的线程时,CLR 将会为该线程准备一个连续的内存区域——托管堆。托管堆将会维护一个指针,该指针的地址将会是下一个对象生成的地址。在托管堆刚生成时,该地址会指向整个堆的基地址。所有的引用类型将会在堆中分配内存。程序生成第一个引用类型对象的时候,该类型的内存将会被分配在托管堆的基地址中。当程序创建下一个对象的时候,GC 会立即在第一个对象的后面分配一段内存,以此类推。
托管堆的内存分配比非托管的内存分配快,托管堆在分配内存时,仅相当于给一个指针加了一个数而已,此过程就像在栈中分配内存一样快。不仅如此,由于托管堆中内存分配是连续的,并且对象存储的位置也是连续的,这样做会加快程序访问对象的速度。
释放内存
GC 的优化引擎将会针对程序选择一个最佳的时间来进行垃圾回收操作。回收内存的时候,程序不用的对象将会被回收。GC 将会检查程序的“根”。每一个托管的应用程序都有一个一组“根”。每一个根都会存一个托管堆中对象的引用,或者被设置为null。程序的根包括static 成员、线程栈中的局部变量与方法的参数、CPU 寄存器。GC 将会拿到一个根的列表,其中包括被 JIT 激活的根和 正在被 CLR 维护的根。GC将会根据这个列表生成一个图,这个图里包含所有可以从根访问到的对象。
如上文所述,不在图中的对象将不会被程序的 “根” 引用,GC 将考虑回收这些对象。在回收的过程中,GC 会先检查托管堆,找到所有无法访问到的对象;之后,GC 会使用内存拷贝功能来整理所有正常的对象,让它们仍在在一个连续的内存空间内;最后一步很重要,GC 会更正程序中的“根”,并且将托管堆中的指针指向最后一个对象的后面。注意,只有当不可访问对象到达一定数量的时候才会进行内存整理的操作。如果托管堆中所有的对象均能正常访问,整理内存的操作是没有必要的。
为了提升性能,运行时会把一个大对象拆分到两个堆中存放,并且,GC将会自动释放大对象的内存。然而,为了防止移动内存中的大对象,这种情况下的内存是未经整理的。
托管堆的生命周期(Generations)和性能
为了优化 GC 的性能,托管堆中的空间被分为三个部分:G0(generation 0,下同),G1和G2。CLR 采用的 GC 方案已经涵盖了软件行业中的各种情况。该方案基于以下前提:
- 在回收托管堆时,只回收其中一部分内存比回收全部内存快
- 较新的对象通常比较老的对象的寿命短
- 较新的对象之间有在彼此之间产生联系的倾向,并几乎在同一时间被程序引用。
GC 会把较新的对象放在 G0 中。在程序运行的初期结束后,G0 中未被回收的对象将会被“升级”,并被移动至 G1 和 G2 中(对象升级的过程将会在下文中讨论)。由于回收部分内存较快,托管堆将会优先回收具体的某一个时期(Generation),而不是回收一整个托管堆。
在 CLR 实际工作中,GC 将会在 G0 没有足够空间时回收内存。当程序在 G0 满了的时候尝试创建对象时,GC 发现 G0 中没有空间了,所以它将会尝试回收 G0 中的空间。
由于最先创建的对象寿命通常会比较短,故G0 中的对象会有更大的几率被回收,所以我们并没有直接回收整个托管堆的空间,这么做是一种比较高效的方式,在 G0 中进行局部的内存回收通常会释放足够的空间给新创建的对象。
在回收 G0 之后,GC 将会整理托管堆中的内存本文“释放内存”一章提到过。由于已经留存下来的对象更倾向于有较长的生命周期,所以 GC 要把 G0 中的对象提升至更高的时期(genearation)中。所以,GC也没有必要在整理 G0 之后再去检查 G1 和 G2 中的对象了。
在 GC 整理完 G0 并且将其中的对象提升至 G1 之后,它将会使用 G0 中剩余的空间来创建新的对象,直到 G0 中又没有剩余空间时,GC 需要决定是否需要回收较老的时期(generation)中的对象。比如,如果 GC 在 G0 中无法回收足够的空间去创建新的对象时,GC 将会先后在 G1 和 G2 中触发 垃圾回收的过程。如果这么做仍然无法满足新创建对象的需求时,GC 将会回收 G2,G1,G0 中的内存,并且将 G0 中的剩余对象全部升级到 G1 中,G1 中剩余的对象全部升级到 G2 中。由于垃圾回收只支持三个时期(generations),所有 G2 中的对象将会继续呆在 G2 中,直到其中的对象变得不可到达时,才回收这些对象。
给非托管代码释放内存
应用程序生成的对象都可以依赖 GC 来完成自动垃圾回收。然而,非托管资源需要自己手动管理对象的生命周期。典型的非托管资源的应用就是文件句柄file handle,窗口句柄window handle,或者网络操作。即使 GC 可以追踪包含非托管对象的托管对象,但是它并不知道到底如何清理非托管的对象。
当你试图在托管对象中中引用非托管的对象时,应该自己实现当前对象的Dispose方法,并且在该方法中清理非托管的对象。你可以在该对象的使用者结束使用该对象的时候释放分配给该对象的内存。当使用包含非托管资源的托管对象时,一定要在必要的时候调用Dispose方法以确保该托管对象已经被释放。关于如何实现Dispose方法,你可以参照  垃圾回收 一文
翻译错误之处,请指出,感激不尽~