.NET中的栈和堆的差异?

每一个.NET应用程序最终都会运行在一个OS(操作系统)进程中,假设这个OS的传统的32位系统,那么每个.NET应用程序理论上都可以拥有一个4GB的虚拟内存。.NET会在这个4GB的虚拟内存块中开辟三块内存作为 堆栈、托管堆 以及 非托管堆.

(1).NET中的堆栈

堆栈用来存储值类型的对象和引用类型对象的引用(地址),其分配的是一块连续的地址,如下图所示,在.NET应用程序中,堆栈上的地址从高位向低位分配内存,.NET只需要保存一个指针指向下一个未分配内存的内存地址即可。

对于所有需要分配的对象,会依次分配到堆栈中,其释放也会严格按照栈的逻辑(FILO,先进后出)依次进行退栈。(这里的“依次”是指按照变量的作用域进行的),假设有以下一段代码:

TempClass a = new TempClass();
a.numA = 1;
a.numB = 2;

其在堆栈中的内存图如下图所示:

.NET中的栈和堆的差异?

这里TempClass是一个引用类型,拥有两个整型的int成员,在栈中依次需要分配的是a的引用,a.numA和a.numB。当a的作用域结束之后,这三个会按照a.numB→a.numA→a的顺序依次退栈。

(2).NET中的托管堆

众所周知,.NET中的引用类型对象时分配在托管堆上的,和堆栈一样,托管堆也是进程内存空间中的一块区域。But,托管堆的内存分配却和堆栈有很大区别。受益于.NET内存管理机制,托管堆的分配也是连续的(从低位到高位),但是堆中却存在着暂时不能被分配却已经无用的对象内存块

当一个引用类型对象被初始时,会通过指向堆上可用空间的指针分配一块连续的内存,然后使堆栈上的引用指向堆上刚刚分配的这块内存块。下图展示了托管堆的内存分配方式:

.NET中的栈和堆的差异?

如上图所示,.NET程序通过分配在堆栈中的引用来找到分配在托管堆的对象实例。当堆栈中的引用退出作用域时,这时仅仅就断开和实际对象实例的引用联系。而当托管堆中的内存不够时,.NET会开始执行GC(垃圾回收)机制。GC是一个非常复杂的过程,它不仅涉及托管堆中对象的释放,而且需要移动合并托管堆中的内存块。当GC之后,堆中不再被使用的对象实例才会被部分释放(注意并不是完全释放),而在这之前,它们在堆中是暂时不可用的。在C/C++中,由于没有GC,因此可以直接free/delete来释放内存。

(3).NET中的非托管堆

.NET程序还包含了非托管堆,所有需要分配堆内存的非托管资源将会被分配到非托管堆上。非托管的堆需要程序员用指针手动地分配和释放内存,.NET中的GC和内存管理不适用于非托管堆,其内存块也不会被合并移动,所以非托管堆的内存分配是按块的、不连续的。因此,这也解释了我们为何在使用非托管资源(如:文件流、数据库连接等)需要手动地调用Dispose()方法进行内存释放的原因。