对象的内存模型
操作系统和运行库通常将用于容纳数据的内存划分为两个独立的区域,每个区域都采取既然不同的方
式来进行管理,这两个区域通常称为堆栈和堆。
调用一个方法时,它的参数以及它的局部变量需要的内存从堆栈中获取,方法结束后(要么返回,要
么抛出异常),为参数和局部变量分配的内存将自动归还给堆栈并可在另一个方法调用时重新使用。
使用new关键字和一次构造函数来创建一个对象(类的实例)时,构建对象所需的内存总是从堆中获取
,使用引用变量,和一个对象可以从几个地方引用,对对象的最后一个引用消失后,对象使用的内存
就可供重用(虽然没有立即被回收)
所有堆类型都是在堆栈上创建的,所有引用类对象时都是在堆上创建的。堆和堆栈来源于运行库对内
存进行组织的方式。
堆栈内存就像一系列堆叠越高的箱子,调用方法时,每个参数都被放入一个箱子,并将这个箱子放到
堆栈最顶部,每个局域变量也同样分配一个箱子,并同样放到堆栈的最顶部,方法结束后,它的箱子
都会从堆栈中移除。
堆内存像散布在房间里的一大堆箱子,而不像堆栈那样每个箱子都严格地叠置在另一个箱子的上方,
每个箱子都有一个标签,标记箱子时候使用,创建一个对象时,运行库会查找一个空箱子,并把它分
配给对象,对象的引用存储在堆栈上的一个局部变量中,运行库将跟踪每一个箱子的引用数量(两个
变量有可能引用同一个对象),一旦最后一个引用消失,运行库就将箱子标记为未使用,将来的某个
时候,会消除箱子里的东西,使其真正能够重用。
void Method (int Param) //Param=42
{
Circle c=new Circle(Param);
}
堆栈中将分配出一小片内存(刚好一个int)
并使用42初始化在方法内部,还要从堆栈中分配处一小块内存,它刚好能存储一个引用,暂时不初始
化(为C准备),接着从堆中分配一块足够大的内存区域,来容纳Circle对象,堆内存是一种有限的资
源,如果堆内存被耗尽,new一个对象时会抛出异常,OutOfMemoryException,创建对象失败。
首先堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每个进程的虚拟内存为4GB)
堆栈stack
堆栈中存储值类型。
堆栈实际上是向下填充,即由高内存地址指向地内存地址填充。
堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。
堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!
堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。
通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆(托管堆)!
堆(托管堆)heap
堆(托管堆)存储引用类型。
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。
比如创建一个对象:
Customer cus;
cus = new Customer();
申明一个Customer的引用cus,在堆栈上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的Customer对象!
cus占4个字节的空间,包含了存储Customer的引用地址。
接着分配堆上的内存以存储Customer对象的实例,假定Customer对象的实例是32字节,为了在堆上找到一个存储Customer对象的存储位置。
.NET运行库在堆中搜索第一个从未使用的,32字节的连续块存储Customer对象的实例!
然后把分配给Customer对象实例的地址赋给cus变量!
从这个例子中可以看出,建立对象引用的过程比建立值变量的过程复杂,且不能避免性能的降低!
实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!
有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。
当一个应用变量出作用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。
1,什么是GC
GC的全称是garbage collection,中文名称垃圾回收,是.net中对内存管理的一种功能。垃圾回收器跟踪并回收托管内存中分配的对象,定期执行垃圾回收以回收分配给没有有效引用的对象的内存。当使用可用内存不能满足内存请求时,GC会自动进行。在进行垃圾回收时,垃圾回收器回首先搜索内存中的托管对象,然后从托管代码中搜索被引用的对象并标记为有效,接着释放没有被标记为有效的对象并收回内存,最后整理内存将有效对象挪动到一起。这就是GC的四个步骤。
由上可见,GC是很影响性能的,所以一般说来这种事情况还是尽量少发生为好。
为了减少一些性能影响,.net的GC支持对象老化,或者说分代的概念,代是对象在内存中相对存现时期的度量单位,对象的代数或存现时期说明对象所属的代。目前.net的垃圾回收器支持三代。每进行一次GC,没有被回收的对象就自动提升一代。较近创建的对象属于较新的代,比在应用程序生命周期中较早创建的对象的代数低。最近代中的对象位于零代中。每一次GC的时候,都首先回收零代中的对象,只有在较低代数的对象回收完成后仍不能满足需求的情况下才回收较高代数的对象。
2,读文章,看到了一个关于堆和栈(堆栈)的区别的比喻。很形象:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
3,GC和堆栈、堆
由前述堆栈和堆的概念可以看出,堆栈不存在垃圾收集的问题,只需要直接压栈即可,而堆,则面临着很复杂的垃圾回收的问题。GC完全是对堆进行操作的,而对堆中对象是否有效的判断则是通过遍历堆栈来实现的。这里涉及到一个引用计数的概念,引用计数是对堆中对象被引用次数的统计,当一个对象的引用计数为零了,那么这个对象就可以被回收了。在进行GC的时候,垃圾回收器遍历堆栈,当发现一个堆地址的时候,它就将堆中该地址上的对象的引用计数加1,然后销毁堆中所有引用计数为零的对象,回收内存并整理堆中的碎片。
4,类实例化的步骤
类是最常见也是我们用的最多的一种引用类型,我们知道实例化一个类使用的是一个我们司空见惯的语句:
ClassA ca = new ClassA();
那么这短短的一句话中,计算机又做了些什么事情呢?
实际上,计算机在这个过程中大致做了这么几件事:
首先,在ClassA ca的时候,生成一个空的引用指针,并将它推入堆栈中:
然后,在new ClassA()的时候,生成ClassA的新的实例,并放入堆中:
在赋值号=这一步,将ca的引用指针指向刚刚生成的新实例:
这个时候,才算完成了整条语句的操作。
5,特例:string 类型与堆
大家知道,string类型是一种引用类型。但它又有一些值类型的特征。比如指向同一个字符串的两个string变量,如果其中一个变量值发生了改变,却不会影响到另外一个string变量。这就是因为:
(1)CLR使用了一种叫字符串驻留的技术,对于
string str1="abc";
string str2="abc";
当CLR初始化时,会创建一个内部的散列表,其中的键为字符串,值为指向托管堆中字符串的引用。刚开始,散列表为空,JIT编译器编译方法时,会在散列表中查找每一个文本常量字符串,首先会查找"abc"字符串,并且因为没有找到,编译器会在托管堆中构造一个新的指向"abc"的String对象引用,然后将"abc"字符串和指向该对象的引用添加到散列表中。
接着,在散列表中查找第二个"abc",这一次由于找到了该字符串,所以编译器不会执行任何操作,代码中再没有其它的文本常量字符串,编译器的任务完成,代码开始执行。执行时,CLR发现第一个语句需要一个"abc"字符串引用,于是,CLR会在内部的散列表中查找"abc",并且会找到,这样指向先前创建的String对象的引用就被保存在变量s1中,执行第二条语句时,CLR会再一次在散列表中查找"abc",并且仍然会找到,指向同一个String对象的引用会被保存在变量s2中,到此s1和s2指向了同一个引用,所以System.Object.Equals(s1,s2)就会返回true了。
(2)当使用重载操作符”=”给string对象赋值时,string的对象是引用类型,它保留在堆上,而不是堆栈上.因此,当把一个字符串赋给另一个字符串时,会得到对内存中同一个字符串的两个引用.例如,修改其中一个字符串,就会创建一个全新的string对象(注意,这个过程发生在”=”中),而另一个字符串没有改变.
6,C#中值类型分配在堆栈中。
值类型:bool,byte,char,decimal,double,enum,float,int,long,sbyte,short,struct,uint,ulong,ushort.
7,C#中引用类型分配在堆中,在堆栈中创建一个指向到堆的引用,返回给声明的变量。
引用类型:class,delegate,interface,object,string
备注:1. 值类型数组虽然分配在堆上,但数组元素依然是值类型,并没有被装箱。
2, 引用对象的值类型成员也随对象一起分配在堆上,同样也还是值类型,没有被装箱