首先堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每一个进程的虚拟内存为4GB)html
堆栈stack程序员
堆栈中存储值类型。算法
堆栈其实是向下填充,即由高内存地址指向低内存地址填充。数据库
堆栈的工做方式是先分配内存的变量后释放(先进后出原则)。编程
堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!windows
堆栈的性能很是高,可是对于全部的变量来讲还不太灵活,并且变量的生命周期必须嵌套。数组
一般咱们但愿使用一种方法分配内存来存储数据,而且方法退出后很长一段时间内数据仍然可使用。此时就要用到堆(托管堆)!缓存
堆(托管堆)heap安全
堆(托管堆)存储引用类型。网络
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不一样,堆是从下往上分配,因此自由的空间都在已用空间的上面。
好比建立一个对象:
Customer cus;
cus = new Customer();
申明一个Customer的引用cus,在堆栈上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的Customer对象!
cus占4个字节的空间,包含了存储Customer的引用地址。
接着分配堆上的内存以存储Customer对象的实例,假定Customer对象的实例是32字节,为了在堆上找到一个存储Customer对象的存储位置。
.NET运行库在堆中搜索第一个从未使用的,32字节的连续块存储Customer对象的实例!
而后把分配给Customer对象实例的地址赋给cus变量!
从这个例子中能够看出,创建对象引用的过程比创建值变量的过程复杂,且不能避免性能的下降!
实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失不少!
有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。
当一个应用变量出做用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。
装箱转化
using System;
class Boxing
{
public static void Main()
{ int i=110;
object obj=i;
i=220;
Console.WriteLine("i={0},obj={1}",i,obj);
obj=330;
Console.WriteLine("i={0},obj={1}",i,obj);
}
}
定义整数类型变量I的时候,这个变量占用的内存是内存栈中分配的,第二句是装箱操做将变量 110存放到了内存堆中,而定义object对象类型的变量obj则在内存栈中,并指向int类型的数值110,而该数值是付给变量i的数值副本。
因此运行结果是
i=220,obj=110
i=220,obj=330
内存格局一般分为四个区
全局数据区:存放全局变量,静态数据,常量
代码区:存放全部的程序代码
栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等,
堆区:即自由存储区
值类型变量与引用类型变量的内存分配模型也不同。为了理解清楚这个问题,读者首
先必须区分两种不一样类型的内存区域:线程堆栈(Thread Stack)和托管堆(Managed Heap)。
每一个正在运行的程序都对应着一个进程(process),在一个进程内部,能够有一个或多
个线程(thread),每一个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保
存自身的一些数据,好比函数中定义的局部变量、函数调用时传送的参数值等,这部份内存
区域的分配与回收不须要程序员干涉。
全部值类型的变量都是在线程堆栈中分配的。
另外一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所
以又称为“托管堆(managed heap)”。
用new 关键字建立的类的对象时,分配给对象的内存单元就位于托管堆中。
在程序中咱们能够随意地使用new 关键字建立多个对象,所以,托管堆中的内存资源
是能够动态申请并使用的,固然用完了必须归还。
打个比方更易理解:托管堆至关于一个旅馆,其中的房间至关于托管堆中所拥有的内存
单元。当程序员用new 方法建立对象时,至关于游客向旅馆预订房间,旅馆管理员会先看
一下有没有合适的空房间,有的话,就能够将此房间提供给游客住宿。当游客旅途结束,要
办理退房手续,房间又能够为其余旅客提供服务了。
从表 1 能够看到,引用类型共有四种:类类型、接口类型、数组类型和委托类型。
全部引用类型变量所引用的对象,其内存都是在托管堆中分配的。
严格地说,咱们常说的“对象变量”实际上是类类型的引用变量。但在实际中人们常常将
引用类型的变量简称为“对象变量”,用它来指代全部四种类型的引用变量。在不致于引发
混淆的状况下,本书也采用了这种惯例。
在了解了对象内存模型以后,对象变量之间的相互赋值的含义也就清楚了。请看如下代
码(示例项目ReferenceVariableForCS):
class A
02 {
03 public int i;
04 }
05 class Program
06 {
07 static void Main(string[] args)
08 {
09 A a ;
10 a= new A();
11 a.i = 100;
12 A b=null;
13 b = a; //对象变量的相互赋值
14 Console.WriteLine("b.i=" + b.i); //b.i=?
15 }
16 }
注意第12 和13 句。
程序的运行结果是:
b.i=100;
请读者思索一下:两个对象变量的相互赋值意味着什么?
事实上,两个对象变量的相互赋值意味着赋值后两个对象变量所占用的内存单元其内容
是相同的。
讲得详细一些:
第10 句建立对象之后,其首地址(假设为“1234 5678”)被放入到变量a 自身的4 个
字节的内存单元中。
第12 句又定义了一个对象变量b,其值最初为null(即对应的4 个字节内存单元中为
“0000 0000”)。
第13 句执行之后,a 变量的值被复制到b 的内存单元中,如今,b 内存单元中的值也为
“1234 5678”。
根据前面介绍的对象内存模型,咱们知道如今变量a 和b 都指向同一个实例对象。
若是经过b.i 修改字段i 的值,a.i 也会同步变化,由于a.i 与b.i 其实表明同一对象的同
一字段。
整个过程能够用图 9 来讲明:
图
图 9 对象变量的相互赋值
由此获得一个重要结论:
对象变量的相互赋值不会致使对象自身被复制,其结果是两个对象变量指向同一对象。
另外,因为对象变量自己是一个局部变量,所以,对象变量自己是位于线程堆栈中的。
严格区分对象变量与对象变量所引用的对象,是面向对象编程的关键之一。
因为对象变量相似于一个对象指针,这就产生了“判断两个对象变量是否引用同一对象”
的问题。
C#使用“==”运算符比对两个对象变量是否引用同一对象,“!=”比对两个对象变量
22
是否引用不一样的对象。参看如下代码:
//a1与a2引用不一样的对象
A a1= new A();
A a2= new A();
Console.WriteLine(a1 == a2);//输出:false
a2 = a1;//a1与a2引用相同的对象
Console.WriteLine(a1 == a2);//输出:true
须要注意的是,若是“==”被用在值类型的变量之间,则比对的是变量的内容:
int i = 0;
int j = 100;
if (i == j)
{
Console.WriteLine("i与j的值相等");
}
理解值类型与引用类型的区别在面向对象编程中很是关键。
一、类型,对象,堆栈和托管堆
C#的类型和对象在应用计算机内存时,大致用到两种内存,一个
叫堆栈,另外一个叫托管堆,下面咱们用直角长方形来表明堆栈,
用圆角长方形来表明托管堆。
首先讨论一下方法内部变量的存放。
先举个例子,有以下两个方法,Method_1 和Add,分别以下:
public void Method_1()
{
int value1=10; //1
int value2=20; //2
int value3=Add(value,value); //3
}
public int Add(int n1,int n2)//4
{
rnt sum=n1+n2;//5
return sum;//6
}
这段代码的执行,用图表示为:
上述的每一个图片,基本对应程序中的每一个步骤。在开始执行Met
hod_1的时候,先把value1 压入堆栈顶,而后是value2,接
下来的是调用方法Add,由于方法有两个参数是n1 和n2,因此
把n1 和n2 分别压入堆栈,由于此处是调用了一个方法,而且方
法有返回值,因此这里须要保存Add的返回地址,而后进入Ad
d方法内部,在Add内部,首先是给sum 赋值,因此把sum 压
入栈项,而后用return 返回,此时,先前的返回地址就起到了
做用,return 会根据地址返回去的,在返回的过程当中,把sum
推出栈顶,找到了返回地址,但在Method_1 方法中,咱们但愿
把Add的返回值赋给value3,此时的返回地址也被推出堆栈,
把value3 压入堆栈。虽这个例子的结果在这里没有多大用途,
但这个例子很好的说明了在方法被执行时,变量与进出堆栈的情
况。这里也能看出为何方法内部的局变量用事后,不能在其余
方法中访问的缘由。
其次来讨论一下类和对象在托管堆和堆栈中的状况。
先看一下代码:
class Car
{
public void Run()
{
Console.WriteLine("一切正常");
}
public virtual double GetPrice()
{
return 0;
}
public static void Purpose()
{
Console.WriteLine("载人");
}
PDF 文件使用 "pdfFactory Pro" 试用版本建立 fw w w . f i n e p rint.cn
}
class BMW : Car
{
public override double GetPrice()
{
return 800000;
}
}
上面是两个类,一个Father一个Son,Son 继承了Father,
由于你类中有一个virtual的BuyHouse 方法,因此Son类能够重
写这个方法。
下面接着看调用代码。
public void Method_A()
{
double CarPrice;//1
Car car = new BMW();//2
CarPrice = car.GetPrice();//调用虚方法(其实调用的是重写后
的方法)
car.Run();//调用实例化方法
Car.Purpose();//调用静态方法
}
这个方法也比较简单,就是定义一个变量用来得到价格,同时
定义了一个父类的变量,用子类来实例化它。
接下来,咱们分步来讲明。
看一下运行时堆栈和托管堆的情部我
这里须要说明的是,类是位于托管堆中的,每一个类又分为四个
类部,类指针,用来关联对象;同步索引,用来完成同步(好比线
程的同步)需创建的;静态成员是属于类的,因此在类中出现,还
有一个方法列表(这里的方法列表项与具体的方法对应)。
当Method_A方法的第一步执行时:
这时的CarPrice 是没有值的
当Method_A方法执行到第二步,其实第二步又能够分红
Car car;
car = new BMW();
先看Car car;
car在这里是一个方法内部的变量,因此被压到堆栈中。
再看 car = new BMW();
这是一个实例化过程,car变成了一个对象
这里是用子类来实例化父类型。对象实际上是子类的类型的,但
变量的类型是父类的。
接下来,在Method_A中的调用的中调用car.GetPrice(),
对于Car来讲,这个方法是虚方法(而且子类重写了它),虚方
法在调用是不会执行类型上的方法,即不会执行Car类中的虚方
法,而是执行对象对应类上的方法,即BMW中的GtPrice。
若是Method_A中执行方法Run(),由于Run是普通实例方
法,因此会执行Car类中的Run 方法。
若是调用了Method_A的Purpose 方法,即不用变量car调
用,也不用对象调用,而是用类名Car调用,由于静态方法会在
类中分配内存的。若是用Car生成多个实例,静态成员只有一份,
就是在类中,而不是在对象中。
33333333333333333333333333333333
在32位的Windows操做系统中,每一个进程均可以使用4GB的内存,这得益于虚拟寻址技术,在这4GB的内存中存储着可执行代码、代码加载的DLL和程序运行的全部变量,在C#中,虚拟内存中有个两个存储变量的区域,一个称为堆栈,一个称为托管堆,托管堆的出现是.net不一样于其余语言的地方,堆栈存储值类型数据,而托管堆存储引用类型如类、对象,并受垃圾收集器的控制和管理。在堆栈中,一旦变量超出使用范围,其使用的内存空间会被其余变量从新使用,这时其空间中存储的值将被其余变量覆盖而不复存在,但有时候咱们但愿这些值仍然存在,这就须要托管堆来实现。咱们用几段代码来讲明其工做原理,假设已经定义了一个类class1:
class1 object1;
object1=new class1();
第一句定义了一个class1的引用,实质上只是在堆栈中分配了一个4个字节的空间,它将用来存府后来实例化对象在托管堆中的地址,在windows中这须要4个字节来表示内存地址。第二句实例化object1对象,其实是在托管堆中开僻了一个内存空间来存储类class1的一个具体对象,假设这个对象须要36个字节,那么object1指向的其实是在托管堆一个大小为36个字节的连续内存空间开始的地址。由此也能够看出在C#编译器中为何不容许使用未实例化的对象,由于这个对象在托管堆中还不存在。当对象再也不使用时,这个被存储在堆栈中的引用变量将被删除,可是从上述机制能够看出,在托管堆中这个引用指向的对象仍然存在,其空间什么时候被释放取决垃圾收集器而不是引用变量失去做用域时。
在使用电脑的过程当中你们可能都有过这种经验:电脑用久了之后程序运行会变得愈来愈慢,其中一个重要缘由就是系统中存在大量内存碎片,就是由于程序反复在堆栈中建立和释入变量,长此以往可用变量在内存中将再也不是连续的内存空间,为了寻址这些变量也会增长系统开销。在.net中这种情形将获得很大改善,这是由于有了垃圾收集器的工做,垃圾收集器将会压缩托管堆的内存空间,保证可用变量在一个连续的内存空间内,同时将堆栈中引用变量中的地址改成新的地址,这将会带来额外的系统开销,可是,其带来的好处将会抵消这种影响,而另一个好处是,程序员将再也不花上大量的心思在内在泄露问题上。
固然,以C#程序中不只仅只有引用类型的变量,仍然也存在值类型和其余托管堆不能管理的对象,若是文件名柄、网络链接和数据库链接,这些变量的释放仍须要程序员经过析构函数或IDispose接口来作。
另外一方面,在某些时候C#程序也须要追求速度,好比对一个含用大量成员的数组的操做,若是仍使用传统的类来操做,将不会获得很好的性能,由于数组在C#中实际是System.Array的实例,会存储在托管堆中,这将会对运算形成大量的额外的操做,由于除了垃圾收集器除了会压缩托管堆、更新引用地址、还会维护托管堆的信息列表。所幸的是C#中一样可以经过不安全代码使用C++程序员一般喜欢的方式来编码,在标记为unsafe的代码块使用指针,这和在C++中使用指针没有什么不一样,变量也是存府在堆栈中,在这种状况下声明一个数组可使用stackalloc语法,好比声明一个存储有50个double类型的数组:
double* pDouble=stackalloc double[50]
stackalloc会给pDouble数组在堆栈中分配50个double类型大小的内存空间,可使用pDouble[0]、*(pDouble+1)这种方式操做数组,与在C++中同样,使用指针时必须知道本身在作什么,确保访问的正确的内存空间,不然将会出现没法预料的错误。
掌握托管堆、堆栈、垃圾收集器和不安全代码的工做原理和方式,将有助于你成为真正的优秀C#程序员。
进程中每一个线程都有本身的堆栈,这是一段线程建立时保留下的地址区域。咱们的“栈内存”即在此。至于“堆”内存,我我的认为在未用new定义时,堆应该就是未“保留”未“提交”的自由空间,new的功能是在这些自由空间中保留(并提交?)出一个地址范围
栈(Stack)是操做系统在创建某个进程时或者线程(在支持多线程的操做系统中是线程)为这个线程创建的存储区域,该区域具备FIFO的特性,在编译的时候能够指定须要的Stack的大小。在编程中,例如C/C++中,全部的局部变量都是从栈中分配内存空间,实际上也不是什么分配,只是从栈顶向上用就行,在退出函数的时候,只是修改栈指针就能够把栈中的内容销毁,因此速度最快。
堆(Heap)是应用程序在运行的时候请求操做系统分配给本身内存,通常是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。因为从操做系统管理的内存分配因此在分配和销毁时都要占用时间,因此用堆的效率低的多!可是堆的好处是能够作的很大,C/C++对分配的Heap是不初始化的。
在Java中除了简单类型(int,char等)都是在堆中分配内存,这也是程序慢的一个主要缘由。可是跟C/C++不一样,Java中分配Heap内存是自动初始化的。在Java中全部的对象(包括int的wrapper Integer)都是在堆中分配的,可是这个对象的引用倒是在Stack中分配。也就是说在创建一个对象时从两个地方都分配内存,在Heap中分配的内存实际创建这个对象,而在Stack中分配的内存只是一个指向这个堆对象的指针(引用)而已。
在.NET的全部技术中,最具争议的恐怕是垃圾收集(Garbage Collection,GC)了。做为.NET框架中一个重要的部分,托管堆和垃圾收集机制对咱们中的大部分人来讲是陌生的概念。在这篇文章中将要讨论托管堆,和你将从中获得怎样的好处。
为何要托管堆?
.NET框架包含一个托管堆,全部的.NET语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,可是全部的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆。
垃圾收集器的基本算法很简单:
● 将全部的托管内存标记为垃圾
● 寻找正被使用的内存块,并将他们标记为有效
● 释放全部没有被使用的内存块
● 整理堆以减小碎片
托管堆优化
看上去彷佛很简单,可是垃圾收集器实际采用的步骤和堆管理系统的其余部分并不是微不足道,其中经常涉及为提升性能而做的优化设计。举例来讲,垃圾收集遍历整个内存池具备很高的开销。然而,研究代表大部分在托管堆上分配的对象只有很短的生存期,所以堆被分红三个段,称做generations。新分配的对象被放在generation 0中。这个generation是最早被回收的——在这个generation中最有可能找到再也不使用的内存,因为它的尺寸很小(小到足以放进处理器的L2 cache中),所以在它里面的回收将是最快和最高效的。
托管堆的另一种优化操做与locality of reference规则有关。该规则代表,一块儿分配的对象常常被一块儿使用。若是对象们在堆中位置很紧凑的话,高速缓存的性能将会获得提升。因为托管堆的天性,对象们老是被分配在连续的地址上,托管堆老是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码造成了鲜明的对比,在标准堆中,堆很容易变成碎片,并且一块儿分配的对象常常分得很远。
还有一种优化是与大对象有关的。一般,大对象具备很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。由于移动大对象所带来的开销超过了整理这部分堆所能提升的性能。
关于外部资源(External Resources)的问题
垃圾收集器可以有效地管理从托管堆中释放的资源,可是资源回收操做只有在内存紧张而触发一个回收动做时才执行。那么,类是怎样来管理像数据库链接或者窗口句柄这样有限的资源的呢?等待,直到垃圾回收被触发以后再清理数据库链接或者文件句柄并非一个好方法,这会严重下降系统的性能。
全部拥有外部资源的类,在这些资源已经再也不用到的时候,都应当执行Close或者Dispose方法。从Beta2(译注:本文中全部的Beta2均是指.NET Framework Beta2,再也不特别注明)开始,Dispose模式经过IDisposable接口来实现。这将在本文的后续部分讨论。
须要清理外部资源的类还应当实现一个终止操做(finalizer)。在C#中,建立终止操做的首选方式是在析构函数中实现,而在Framework层,终止操做的实现则是经过重载System.Object.Finalize 方法。如下两种实现终止操做的方法是等效的:
~OverdueBookLocator()
{
Dispose(false);
}
和:
public void Finalize()
{
base.Finalize();
Dispose(false);
}
在C#中,同时在Finalize方法和析构函数实现终止操做将会致使错误的产生。
除非你有足够的理由,不然你不该该建立析构函数或者Finalize方法。终止操做会下降系统的性能,而且增长执行期的内存开销。同时,因为终止操做被执行的方式,你并不能保证什么时候一个终止操做会被执行。
内存分配和垃圾回收的细节
对GC有了一个整体印象以后,让咱们来讨论关于托管堆中的分配与回收工做的细节。托管堆看起来与咱们已经熟悉的C++编程中的传统的堆一点都不像。在传统的堆中,数据结构习惯于使用大块的空闲内存。在其中查找特定大小的内存块是一件很耗时的工做,尤为是当内存中充满碎片的时候。与此不一样,在托管堆中,内存被组制成连续的数组,指针老是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单地递增——由此而来的一个好处是,分配操做的效率获得了很大的提高。
当对象被分配的时候,它们一开始被放在generation 0中。当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操做被触发。因为generation 0的大小很小,所以这将是一个很是快的GC过程。这个GC过程的结果是将generation 0完全的刷新了一遍。再也不使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。
当generation 1的大小随着从generation 0中移入的对象数量的增长而接近它的上限的时候,一个回收动做被触发来在generation 0和generation 1中执行GC过程。如同在generation 0中同样,再也不使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,由于在generation 0中最有可能存在大量的已再也不使用的临时对象。对generation 2的回收过程具备很高的开销,而且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。若是对generation 2的GC过程仍然不能释放足够的内存,那么系统就会抛出OutOfMemoryException异常
带有终止操做的对象的垃圾收集过程要稍微复杂一些。当一个带有终止操做的对象被标记为垃圾时,它并不会被当即释放。相反,它会被放置在一个终止队列(finalization queue)中,此队列为这个对象创建一个引用,来避免这个对象被回收。后台线程为队列中的每一个对象执行它们各自的终止操做,而且将已经执行过终止操做的对象从终止队列中删除。只有那些已经执行过终止操做的对象才会在下一次垃圾回收过程当中被从内存中删除。这样作的一个后果是,等待被终止的对象有可能在它被清除以前,被移入更高一级的generation中,从而增长它被清除的延迟时间。
须要执行终止操做的对象应当实现IDisposable接口,以便客户程序经过此接口快速执行终止动做。IDisposable接口包含一个方法——Dispose。这个被Beta2引入的接口,采用一种在Beta2以前就已经被普遍使用的模式实现。从本质上讲,一个须要终止操做的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操做,就象下面这个程序片段所演示的那样:
public class OverdueBookLocator: IDisposable
{
~OverdueBookLocator()
{
InternalDispose(false);
}
public void Dispose()
{
InternalDispose(true);
}
protected void InternalDispose(bool disposing)
{
if(disposing)
{
GC.SuppressFinalize(this);
// Dispose of managed objects if disposing.
}
// free external resources here
}
}
在.NET的全部技术中,最具争议的恐怕是垃圾收集(Garbage Collection,GC)了。做为.NET框架中一个重要的部分,托管堆和垃圾收集机制对咱们中的大部分人来讲是陌生的概念。在这篇文章中将要讨论托管堆,和你将从中获得怎样的好处。 为何要托管堆? .NET框架包含一个托管堆,全部的.NET语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,可是全部的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆。 垃圾收集器的基本算法很简单: ● 将全部的托管内存标记为垃圾 ● 寻找正被使用的内存块,并将他们标记为有效 ● 释放全部没有被使用的内存块 ● 整理堆以减小碎片 托管堆优化 看上去彷佛很简单,可是垃圾收集器实际采用的步骤和堆管理系统的其余部分并不是微不足道,其中经常涉及为提升性能而做的优化设计。举例来讲,垃圾收集遍历整个内存池具备很高的开销。然而,研究代表大部分在托管堆上分配的对象只有很短的生存期,所以堆被分红三个段,称做generations。新分配的对象被放在generation 0中。这个generation是最早被回收的——在这个generation中最有可能找到再也不使用的内存,因为它的尺寸很小(小到足以放进处理器的L2 cache中),所以在它里面的回收将是最快和最高效的。 托管堆的另一种优化操做与locality of reference规则有关。该规则代表,一块儿分配的对象常常被一块儿使用。若是对象们在堆中位置很紧凑的话,高速缓存的性能将会获得提升。因为托管堆的天性,对象们老是被分配在连续的地址上,托管堆老是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码造成了鲜明的对比,在标准堆中,堆很容易变成碎片,并且一块儿分配的对象常常分得很远。 还有一种优化是与大对象有关的。一般,大对象具备很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。由于移动大对象所带来的开销超过了整理这部分堆所能提升的性能。 关于外部资源(External Resources)的问题 垃圾收集器可以有效地管理从托管堆中释放的资源,可是资源回收操做只有在内存紧张而触发一个回收动做时才执行。那么,类是怎样来管理像数据库链接或者窗口句柄这样有限的资源的呢?等待,直到垃圾回收被触发以后再清理数据库链接或者文件句柄并非一个好方法,这会严重下降系统的性能。 全部拥有外部资源的类,在这些资源已经再也不用到的时候,都应当执行Close或者Dispose方法。从Beta2(译注:本文中全部的Beta2均是指.NET Framework Beta2,再也不特别注明)开始,Dispose模式经过IDisposable接口来实现。这将在本文的后续部分讨论。 须要清理外部资源的类还应当实现一个终止操做(finalizer)。在C#中,建立终止操做的首选方式是在析构函数中实现,而在Framework层,终止操做的实现则是经过重载System.Object.Finalize 方法。如下两种实现终止操做的方法是等效的: ~OverdueBookLocator() { Dispose(false); } 和: public void Finalize() { base.Finalize(); Dispose(false); } 在C#中,同时在Finalize方法和析构函数实现终止操做将会致使错误的产生。 除非你有足够的理由,不然你不该该建立析构函数或者Finalize方法。终止操做会下降系统的性能,而且增长执行期的内存开销。同时,因为终止操做被执行的方式,你并不能保证什么时候一个终止操做会被执行。 内存分配和垃圾回收的细节 对GC有了一个整体印象以后,让咱们来讨论关于托管堆中的分配与回收工做的细节。托管堆看起来与咱们已经熟悉的C++编程中的传统的堆一点都不像。在传统的堆中,数据结构习惯于使用大块的空闲内存。在其中查找特定大小的内存块是一件很耗时的工做,尤为是当内存中充满碎片的时候。与此不一样,在托管堆中,内存被组制成连续的数组,指针老是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单地递增——由此而来的一个好处是,分配操做的效率获得了很大的提高。 当对象被分配的时候,它们一开始被放在generation 0中。当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操做被触发。因为generation 0的大小很小,所以这将是一个很是快的GC过程。这个GC过程的结果是将generation 0完全的刷新了一遍。再也不使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。 当generation 1的大小随着从generation 0中移入的对象数量的增长而接近它的上限的时候,一个回收动做被触发来在generation 0和generation 1中执行GC过程。如同在generation 0中同样,再也不使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,由于在generation 0中最有可能存在大量的已再也不使用的临时对象。对generation 2的回收过程具备很高的开销,而且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。若是对generation 2的GC过程仍然不能释放足够的内存,那么系统就会抛出OutOfMemoryException异常 带有终止操做的对象的垃圾收集过程要稍微复杂一些。当一个带有终止操做的对象被标记为垃圾时,它并不会被当即释放。相反,它会被放置在一个终止队列(finalization queue)中,此队列为这个对象创建一个引用,来避免这个对象被回收。后台线程为队列中的每一个对象执行它们各自的终止操做,而且将已经执行过终止操做的对象从终止队列中删除。只有那些已经执行过终止操做的对象才会在下一次垃圾回收过程当中被从内存中删除。这样作的一个后果是,等待被终止的对象有可能在它被清除以前,被移入更高一级的generation中,从而增长它被清除的延迟时间。 须要执行终止操做的对象应当实现IDisposable接口,以便客户程序经过此接口快速执行终止动做。IDisposable接口包含一个方法——Dispose。这个被Beta2引入的接口,采用一种在Beta2以前就已经被普遍使用的模式实现。从本质上讲,一个须要终止操做的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操做,就象下面这个程序片段所演示的那样: public class OverdueBookLocator: IDisposable { ~OverdueBookLocator() { InternalDispose(false); } public void Dispose() { InternalDispose(true); } protected void InternalDispose(bool disposing) { if(disposing) { GC.SuppressFinalize(this); // Dispose of managed objects if disposing. } // free external resources here } }
这些都是.NET中CLR的概念,和C#没多大关系。
使用基于CLR的语言编译器开发的代码称为托管代码。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
认真看MSDN Library,就会搞清楚这些概念。
如下代码说明的很形象:
//引用类型('class' 类类型)
class SomeRef { public int32 x;}
//值类型('struct')
struct SomeVal(pulic Int32 x;}
static void ValueTypeDemo()
{
SomeRef r1=new SomeRef();//分配在托管堆
SomeVal v1=new SomeVal();//堆栈上
r1.x=5;//解析指针
v1.x=5;//在堆栈上修改
SomeRef r2=r1;//仅拷贝引用(指针)
SomeVal v2=v1;//先在堆栈上分配,而后拷贝成员
r1.x=8;//改变了r1,r2的值
v1.x=9;//改变了v1,没有改变v2
}
4444444444444444444444444444444444444444444444444444444
栈是内存中彻底用于存储局部变量或成员字段(值类型数据)的高效的区域,但其大小有限制。
托管堆所占内存比栈大得多,当访问速度较慢。托管堆只用于分配内存,通常由CLR(Common Language Runtime)来处理内存释放问题。
当建立值类型数据时,在栈上分配内存;
当建立引用型数据时,在托管堆上分配内存并返回对象的引用。注意这个对象的引用,像其余局部变量同样也是保存在栈中的。该引用指向的值则位于托管堆中。
若是建立了一个包含值类型的引用类型,好比数组,其元素的值也是存放在托管堆中而非栈中的。当从数组中检索数据时,得到本地使用的元素值的副本,而该副本这时候就是存放在栈中的了。因此,不能笼统的说“值类型保存在栈中,引用类型保存在托管堆中”。
值类型和引用类型的区别:引用类型存储在托管堆的惟一位置中,其存在于托管堆中某个地方,由使用该实体的变量引用;而值类型存储在使用它们的地方,有几处在使用,就有几个副本存在。
对于引用类型,若是在声明变量的时候没有使用new运算符,运行时就不会给它分配托管堆上的内存空间,而是在栈上给它分配一个包含null值的引用。对于值类型,运行时会给它分配栈上的空间,并调用默认的构造函数,来初始化对象的状态。
55555555555555555555555555555555555555555555555555
1、栈和托管堆
通用类型系统(CTS)区分两种基本类型:值类型和引用类型。它们之间的根本区别在于它们在内存中的存储方式。.NET使用两种不一样的物理内存块来存储数据—栈和托管堆。以下图所示:
二 类型层次结构
CTS定义了一种类型层次结构,该结构不只仅描述了不一样的预约义类型,还指出了用户定义类型在层次结构种的
文章来自:http://www.cnblogs.com/shenfengok/archive/2011/09/06/2169306.html