了解.NET的内存管理机制,首先应该从内存分配开始,也就是对象的创建环节。对象的创建,是个复杂的过程,主要包括内存分配和初始化两个环节。
例如,对象的创建过程可以表示为: FileStream fs = new FileStream(@"C:"temp.txt", FileMode.Create);
通过new关键字操作,即完成了对FileStream类型对象的创建过程,这一看似简单的操作背后,却经历着相当复杂的过程和周折。
1、 内存分配
CLR(公共语言运行时,Common Language Runtime)管理内存的区域,主要有三块:
一、线程的堆栈:用于分配值类型实例。堆栈主要由操作系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。
二、GC堆:用于分配小对象实例,如果引用类型对象的实例大小小于85000字节,实例将被分配在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩。
三、LOH(Large Object Heap)堆:用于分配大对象实例,如果引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆不会被压缩,而且只在完全GC回收时被回收。
2、堆栈的内存分配机制
对于值类型来说,一般创建在线程的堆栈上。但并非所有的值类型都创建在线程的堆栈上,例如作为类的字段时,值类型作为实例成员的一部分也被创建在托管堆上;装箱发生时,值类型字段也会拷贝在托管堆上。
对于分配在堆栈上的局部变量来说,操作系统维护着一个堆栈指针来指向下一个自由空间的地址,并且堆栈的内存地址是由高位到低位向下填充。以下例而言:
public static void Main() {
int x = 100;
char c = 'A';
}
假设线程栈的初始化地址为50000,因此堆栈指针首先指向50000地址空间。代码由入口函数Main开始执行,首先进入作用域的是整型局部变量x,它将在栈上分配4Byte的内存空间,因此堆栈指针向下移动4个字节,则值100将保存在49997~50000单位,而堆栈指针表示的下一个自由空间地址为49996。
接着进入下一行代码,将为字符型变量c分配2Byte的内存空间,堆栈指针向下移动2个字节至49994单位,值’A’会保存在49995~49996单位,
最后,执行到Main方法的右括号,方法体执行结束,变量x和c的作用域也随之结束,需要删除变量x和c在堆栈内存中的值,其释放过程和分配过程刚好相反:首先删除c的内存,堆栈指针向上递增2个字节,然后删除x的内存,堆栈指针继续向上递增4个字节,程序执行结束,此时内存状况跟开始时一样,地址为50000.
其他较复杂的分配过程,可能在作用域和分配大小上有所不同,但是基本过程大同小异。 栈上的内存分配,效率较高,但是内存容量不大,同时变量的生存周期随着方法的结束而消亡。
问题:值类型中的引用类型字段和引用类型中的值类型字段,其分配情况又是如何?
这一思考其实是一个问题的两个方面:对于值类型嵌套引用类型的情况,引用类型变量作为值类型的成员变量,在堆栈上保存该成员的引用,而实际的引用类型仍然保存在GC堆上;对于引用类型嵌套值类型的情况,则该值类型字段将作为引用类型实例的一部分保存在GC堆上。对于值类型,你只要记着它总是分配在声明它的地方。
问题: 静态字段的内存分配和释放,又有何不同?
静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从创建到AppDomain卸载。因此一个类型无论创建多少个对象,其静态字段在内存中也只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在类型任何对象创建前,或者在任何静态字段或方法被引用前执行。