当前位置:首页 > Windows程序 > 正文

C# 堆栈和堆 Heap Stack

2021-05-25 Windows程序

首先堆栈和堆(托管堆)都在进程的虚拟内存中。(在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运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!

有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。

当一个应用变量出作用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。

装箱转化

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的值相等");

}

理解值类型与引用类型的区别在面向对象编程中非常关键。

1、类型,对象,堆栈和托管堆

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

温馨提示: 本文由Jm博客推荐,转载请保留链接: https://www.jmwww.net/file/71105.html