堆栈和托管堆

  • Post author:
  • Post category:其他



内存格局

通常分为四个区

全局数据区:存放全局变量,静态数据,常量

代码区:存放所有的程序代码

栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等,

堆区:即自由存储区


值类型变量与引用类型变量

的内存分配模型也不一样。为了理解清楚这个问题,首先必须区分两种不同类型的内存区域:

线程堆栈(Thread Stack)和托管堆(Managed Heap)。


每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。所有值类型的变量都是在线程堆栈中分配的。

另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。用new 关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。

在程序中我们可以随意地使用new 关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。

打个比方更易理解:托管堆相当于一个旅馆,其中的房间相当于托管堆中所拥有的内存单元。当程序员用new 方法创建对象时,相当于游客向旅馆预订房间,旅馆管理员会先看有没有合适的空房间,有的话,就可以将此房间提供给游客住宿。当游客旅途结束,要办理退房手续,房间又可以为其他旅客提供服务了。


引用类型共有四种:类类型、接口类型、数组类型和委托类型。


所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。严格地说,我们常说的“对象变量”其实是类类型的引用变量。但在实际中人们经常将引用类型的变量简称为“对象变量”,用它来指代所有四种类型的引用变量。

在了解了对象内存模型之后,对象变量之间的相互赋值的含义也就清楚了。请看以下代

码(示例项目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 其实代表同一对象的同一字段。

由此得到一个重要结论:


对象变量的相互赋值不会导致对象自身被复制,其结果是两个对象变量指向同一对象。

另外,由于对象变量本身是一个局部变量,因此,对象变量本身是位于线程堆栈中的。

由于对象变量类似于一个对象指针,这就产生了“判断两个对象变量是否引用同一对象”

的问题。

C#使用“==”运算符比对两个对象变量是否引用同一对象,“!=”比对两个对象变量

是否引用不同的对象。参看以下代码:

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

}

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


一、栈和托管堆

通用类型系统(CTS)区分两种基本类型:值类型和引用类型。它们之间的根本区别在于它们在内存中的存储方式。

值类型总是在内存中占用一个预定义的字节数(例如,int类型占4个字节,而string类型占用的字节数会根据字符串的长度不同而不同),当声明一个值类型变量时,会在栈中分配适当大小的内存(除了引用类型的值类型成员外,如类的int字段),内存中的这个空间用来存储变量所含的值。.NET维护一个栈指针,它包含栈中下一个可用内存空间的地址。当一个变量离开作用域时,栈指针向下移动被释放变量所占用的字节数,所以它仍指向下一个可用地址。

引用变量也利用栈,但这时栈包含的只是对另一个内存位置的引用,而不是实际值。这个位置是托管堆中的一个地址。和栈一样,它也维护一个指针,包含堆中下一个可用内存空间的地址。但是,堆不是先入后出的,因为对对象的引用可在我们的程序中传递(例如,作为参数传递给方法调用),堆中的对象不会在程序的一个预定点离开作用域。为了在不使用在堆中分配的内存时将它释放,.NET定期执行垃圾收集。垃圾收集器递归地检查应用程序中所有的对象引用。引用不再有效的对象使用的内存无法从程序中访问,该内存就可以回收。

二、类型层次结构

CTS定义了一种类型层次结构,该结构不仅描述了不同的预定义类型,还指出用户定义类型在层次结构中的位置。

三、引用类型

引用类型包含一个指针,指向堆中存储对象本身的位置。因为引用类型只包含引用,不包含实际的值,对方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。

下图显示了声明一个字符串变量并把它作为参数传递给一个方法时所发生的事情。


当声明字符串变量s1时,一个值被压入栈中,它指向栈中的一个位置。在上图中,引用存放在地址1243044中,而实际的字符串存放在堆的地址12262032中。当该字符串传递给一个方法时,在栈上对应输入参数声明了一个新的变量(这次是在地址1243032上,保存在引用变量,即堆中内存位置中的值被传递给这个新的变量。

委托是引用方法的一种引用类型,类似于C++中的函数指针(两者的主要区别在于委托包括调用其方法的对象)。

四、预定义的引用类型

有两种引用类型在C#中受到了特别的重视,它们的C#别名和预定义值类型的C#别名很相像。第一种是Object类(C#别名是object, o小写)。这是所有值类型和引用类型的最终基类。因为所有的类型派生自Object,所以可以把任何类型转换为Object类型,甚至值类型也可以转换。这个把值类型转换为Object的过程称为装箱。所有的值类型都派生自引用类型,在这件看似矛盾的事情背后,装箱的作用不可或缺。

第二种是String类。字符串代表一个固定不变的Unicode字符序列。这种不变性意味着,一旦在堆中分配了一个字符串,它的值将永远不会改变。如果值改变了,.NET就创建一个全新的String对象,并把它赋值给该变量。这意味着,字符串在很多方面都像值类型,而不像引用类型。如果把一个字符串传递给方法,然后在方法体内改变参数的值,这不会影响最初的字符串(当然,除非参数是按引用传递的)。C#提供了别名string(s小写)来代表System.String类。如果在代码中使用String,必须在代码一开始添加using System; 这一行。使用内建的别名string则不需要添加using System;

五、强制类型转换

long x=12345;

int k=(int) x; //发生收缩型强制类型转换

从较小数据类型到较大数据类型的转换称为扩展转换,否则称为收缩转换。编译器能进行隐式的扩展转换,对于收缩转换必须进行显式的强制性转换。因为收缩转换会导致丢失数据,在转换前我们要检查实际值是否超出目标类型的范围。另一个办法是使用checked运算符,如果转换时丢失数据将抛出一个错误。

强制类型转换即可针对值类型,又可针对引用类型。

六、装箱和拆箱(boxing/unboxing)

值类型和引用类型都是从Object类派生的。这意味着任何一个以对象为参数的方法,都可以给它传递一个值类型。相似地,值类型可以调用一个Object类方法:

int j=4;

string str=j.ToString();

这里是另一个强制类型转换的例子。您可能还记得,一个值类型变量包含存储在栈中的数据。您也许不明白值类型的变量如何调用一个引用类型的方法。答案是在一个称为装箱(boxing)的过程中,值类型变量被隐式转换为引用类型。从概念上来讲,装箱的过程就是对应值类型创建一个临时的引用类型的“箱子”。下面是IL代码:

IL_000: ldc.i4.4                //Load the int 4 onto the stack

IL_001: stloc.0                //Pop the value off the stack and into V_0

IL_002: ldloca.s    V_0      //Push the address of variable V_0 onto the stack

//Call Int32::ToString()

IL_004: call      instance string[mscorlib]System.Int32::ToString()

关键的语句是ldloca.s   V_0,它加载指向V_0变量的一个托管指针。ToString()方法是在这个托管指针上调用,而不是在值本身调用。

还可以以下面正常的转换语法显式地将一个值装箱:

int j=4;

object ojb=(object) j;

使用相同的类型转换语法可以把装箱的变量转换回值类型:

int k=(int)obj;

对拆箱操作有一些限制。只能将显式装箱的变量进行拆箱。正常的强制转换中的限制在这里也适用。例如,如果把一个long型值装箱为一个对象,我们不能把该对象拆箱为一个int型值,虽然在拆箱后可以显式地把long转换为int:

long x=1000;

object obj=(object) x;

int i=(int)((long)obj);

装箱与拆箱示意图:


装箱转化例子

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