一、前言
.NET提供了垃圾回收机制,使程序员从内存管理中被解放出来。但这并不代表程序员就无须了解分配的对象是如何被回收的。更重要的是,一些非托管的资源仍然需要程序员小心地分配与回收。
理解堆和堆栈是理解内存管理的基础。每一个.NET程序都最终会运行在一个操作系统进程中,假设这个操作系统是传统的32位的,那每个.NET程序都可以拥有一个4GB的虚拟内存。.NET会在这个4GB的内存块中开辟出三块内存分别作为堆栈、受托管的堆和非托管的堆。
二、.NET中的堆栈
.NET中的堆栈用来存储值类型的对象和引用类型对象的引用,堆栈的分配是连续的,在.NET程序中,始终存储了一个特殊的指针指向堆栈的尾部,这样一个堆栈内存的分配就直接从这个指针指向的内存位置开始向下分配。下图展示了.NET的堆栈分配方式。
如上图所示,堆栈上的地址从高位开始往低位分配内存,.NET只需要保存一个堆栈指针指向下一个未分配内存的内存地址。对于所有需要分配的对象,依次分配到堆栈中,其释放也严格按照栈的逻辑,依次进行退栈。这里提到的“依次”,是指按照变量的作用域进行的。考虑下面的代码:
ClassA a = new ClassA(); a.intA = 1; a.intB = 2;
这里假设ClassA是一个引用类型,则堆栈中依次需要分配的是a的引用、a.intA和a.intB。当a的作用域结束后,这三个变量则从堆栈中依次退出:a.intB、a.intA,然后才是a。
三、.NET中的托管堆
接下来我们看一下托管的堆。.NET中的引用类型对象是分配在托管堆上的。通常我们称.NET中的堆,指的就是托管的堆。和堆栈一样,托管的堆也是进程内存空间中的一块区域。但托管堆中的内存的分配却和堆栈有很大的区别。受益于.NET的内存管理机制,托管堆的分配也是连续的,但是堆中存在着暂时不能被分配却已经无用的对象内存块。当一个引用类型对象被初始化时,就会通过指向堆上可用空间的指针分配一块连续的内存,然后使堆栈上的引用指向堆上的这块内存块。下图展示了堆的分配方式。
如上图所示,程序通过分配在堆栈中的引用来找到分配在托管堆的对象实例。当堆栈区域中的引用退出作用域时,就仅仅断开引用和实际对象的联系。而当托管堆中的内存不够时,.NET开始执行垃圾回收。垃圾回收是一个非常复杂的过程,它不仅涉及托管堆中对象的释放,而且需要移动合并托管堆中的内存块。当垃圾回收后,堆内不被使用的对象才会被部分释放,而在这之前,它们在堆内是暂时不可用的。
四、.NET中的非托管堆
.NET的程序还包含了非托管的堆,所有需要分配堆内存的非托管资源将会被分配到非托管堆上。非托管的堆需要程序员用指针手动地分配并且手动地释放,.NET的垃圾回收和内存管理制度不适用于非托管的堆。
五、堆栈、托管堆和非托管堆的比较
堆栈、托管堆和非托管堆的分配各有特点。堆栈的内存是连续分配的,按照作用域依次分配和释放。堆栈的机制非常简单,.NET依靠一个堆栈指针就可以进行内存操作,分配一个对象和释放一个对象的大部分操作就是自增或者自减堆栈指针。.NET中的值类型对象和引用类型对象的引用是分配在堆栈内的。
托管堆的内存分配虽然也是连续的,但它却比堆栈复杂得多。一块堆内存的分配需要涉及很多.NET内存管理机制的内部操作,另外当堆内存不够时,垃圾回收的执行代价也是非常大的。相对于堆栈来说,堆的分配效率低得多。.NET中的引用类型对象是分配在托管堆上的,这些对象通过分配在堆栈上的引用来进行访问。
非托管堆和托管堆的区别在于非托管堆不受.NET的管理。非托管堆的内存是由程序员手动分配和释放的,垃圾回收机制不适用于非托管堆,内存块也不会被合并移动,所以非托管堆的内存分配按块的,不连续的。
六、总结
.NET程序在进程内存中分配出堆栈、托管堆和非托管堆。所有的值类型对象和引用类型对象的引用都分配在堆栈上,堆栈根据对象的生存周期来依次分配和释放,堆栈根据一个指向栈尾的指针来分配内存,效率很高。
.NET所有的引用类型对象分配在托管堆上,托管堆连续分配内存,并且受.NET的垃圾收集机制管理,受托管堆的内存分配和释放涉及复杂的内存管理,效率相对于堆栈来说低得多。
需要分配堆内存的非托管类型将被分配在非托管堆上,非托管堆不受.NET垃圾收集机制管理,内存块完全由程序员手动申请和释放。