.NET框架自动内存管理(2)
2..NET垃圾回收原理
托管代码则不同,.NET框架为托管对象(Managed Object)提供了完善的内存回收机制。当托管对象不可访问(unreachable)之后,即可被垃圾回收器回收。
托管对象何时被认为是“不可访问的”呢?运行库通过一套复杂的机制来跟踪对象被引用的情况,当应用程序不再有指向对象的引用时,对象就是不可访问的。在以下情况下,对象的引用不再指向对象:
对象的引用变量不可见(即程序执行到对象的引用变量作用域之外)时;
显式的将对象的引用变量置为空值(null),或者将引用变量指向其他对象,使对象与其原来的引用变量之间关系脱开。
如果希望对象能够早些被回收,则只要简单的将其引用变量置为空值,对象就有可能尽早被回收,但真正的回收需要等到相应的垃圾回收周期开始时才能进行。如以下C#代码所示:
public class Caller {
public CallerMethod() {
Foo foo = new Foo();
foo.FooMethod();
......
Foo = null;
......
}
......
}
public class Foo {
......
}
运行库从托管堆(Managed Heap)中为对象分配内存(因此称为托管对象)。垃圾回收器使用“代(Generation)”来表示托管对象在内存中的生存周期,并以此确定哪些对象应该被优先回收。垃圾回收器将托管堆分为三代:第0代(gen0)存放新创建的对象,第1代(gen1)存放生存期较长的对象,第2代(gen2)则存放长期存活的对象。垃圾回收器根据以下假定将对象存放在不同代的托管堆并执行垃圾回收:
新建的对象,它的生存周期可能较短;
在内存中存留时间越长的对象,越有可能继续被引用,因而生存期越长;
因此,垃圾回收器对第0代对象的回收周期最短,第1代次之,第2代对象的回收周期最长。新建的对象首先在gen0堆中分配内存,当gen0堆满时触发垃圾回收。由于gen0堆大部分对象的生存周期都很短,只有少量对象在垃圾回收后存活下来,并被提升到第1代,gen0堆被清空。同理,gen1堆满时触发gen1垃圾回收,存活下来的第1代对象被提升到第2代;gen2堆满时触发gen2垃圾回收,但这时存活下来的第2代对象仍然在gen2堆,即不进行代提升,因为运行库的托管堆最多只有三代。对老一代的内存回收操作包括对所有较新一代的内存回收,在这种情况下,gen1垃圾回收同时也进行gen0垃圾回收,gen2垃圾回收则同时进行gen1和gen0垃圾回收。显然,回收部分托管堆的内存比回收所有托管堆的内存效率要高,这种按代进行垃圾回收的算法提高了垃圾回收的效率,从而保证应用程序的性能。
垃圾回收器有自己独立的工作线程,当进行垃圾回收时不允许应用程序在这些对象上进行任何操作。因此垃圾回收器线程启动时将挂起所有当前正在执行的线程,直至垃圾回收线程结束。在垃圾对象回收后,存活的对象发生代跃迁时,垃圾回收器线程压缩并移动任何空闲的内存,清除内存碎片,保持连续的空闲内存空间,提高内存分配效率。因此垃圾回收器需要较大的系统资源开销,代数越高的垃圾回收,系统开销越大。
垃圾回收器执行垃圾回收的周期是不固定的,最佳回收时间由垃圾回收器引擎根据上述规则自动计算,应用程序无法预计运行库何时进行垃圾回收。在大多数情况下,应该由垃圾回收器确定执行回收的最佳时机。但是,在某些大量消耗内存的关键应用中,及时回收内存对应用程序的性能是至关重要的。这时需要在应用程序中控制非托管资源的释放时机,或者强制垃圾回收器执行垃圾回收周期。
垃圾回收器线程在以下情况下启动:
托管内存堆已满,启动相应代的垃圾回收;
应用程序执行了强制的垃圾回收;
运行库卸载应用程序域;
运行库正在被卸载。