C# 堆栈和托管堆

堆栈 Stack

堆栈中存储值类型。
堆栈实际上是向下填充,即由高内存地址指向低内存地址填充。
堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。
堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!
堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。
通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆(托管堆)!

堆(托管堆)Heap

堆(托管堆)存储引用类型。
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。

比如创建一个对象:

1
2
Customer cus;
cus = new Customer();

申明一个 Customer 的引用 cus,在堆栈上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的 Customer 对象!

cus 占4个字节的空间,包含了存储 Customer 的引用地址。

接着分配堆上的内存以存储 Customer 对象的实例,假定 Customer 对象的实例是32字节,为了在堆上找到一个存储 Customer 对象的存储位置。

.NET 运行库在堆中搜索第一个从未使用的,32字节的连续块存储 Customer 对象的实例!

然后把分配给 Customer 对象实例的地址赋给 cus 变量!

从这个例子中可以看出,建立对象引用的过程比建立值变量的过程复杂,且不能避免性能的降低!

实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!

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

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

内存格局通常分为四个区

  1. 全局数据区:存放全局变量,静态数据,常量
  2. 代码区:存放所有的程序代码
  3. 栈区:存放为运行而分配的局部变量,参数、返回数据,返回地址等,
  4. 堆区:即自由存储区

值类型区分两种不同的内存区域:线程堆栈(Thread Stack)和托管堆(Manged Heap)。

每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或者多个线程(thread),

每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中自定义的局部变量、函数调用时传送的参数值等,这部分内存域与回收不需要程序员干涉。

所有值类型的变量都是在线程堆栈中分配的。

另一块内存区域称为“堆(heap)”,在.Net这种托管环境下,堆由CLR进行管理,所以又称为“托管堆(manged heap)”。

用new 关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。

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

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

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

所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。

严格地说,我们常说的“对象变量”其实是类类型的引用变量。但在实际人们经常将引用类型的变量简称为“对象变量”,用它来指代所有四种类型的引用变量。在不致于引起混淆的情况下,我们也这么认为

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
public int i;
}

class Program {
static void Main(string[] args) {
A a;
a = new A();
a.i = 100;
A b = null;
b = a; //对象变量的相互赋值
Console.WriteLine("b.i = " + b.i); // b.i = ?
}
}

程序的运行结果是:

1
b.i = 100;

事实上,两个对象变量的相互赋值意味着赋值后两个对象变量所占有的内存单元其内容是相同的。

详细一些:

当创建对象以后,其首地址(假设为“1234 5678”)被放入到变量a自身的4个字节的内存单元中。

有定义了一个对象变量b,其值最初为null(即对应的4个字节内存单元中为“0000 0000”)

a变量的值被复制到b的内存单元中,现在,b内存单元的值也为“1234 5678”

根据上面介绍的对象内存模型,我们知道现在变量a和b都指向同一个实例对象。

如果通过b.i修改字段i的值,a.i也会同步变化,因为a.i与b.i其实代表同一对象的统一字段。

由此得到一个重要结论:

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

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

严格区分对象变量于对象变量所引用的对象,是面向对象编程的关键技术之一。

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

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

1
2
3
4
5
A a1=new A();
A a2=new A();
Console.WriteLine(a1 == a2); // 输出:false
a2 = a1; // a1和a2引用相同的对象
Console.WriteLine(a1 == a2); // 输出 true

需要注意的是,如果“==”被用到值类型的变量之间,则比对的变量的内容:

1
2
3
4
5
int i=0;
int j=100;
if(i==j) {
Console.WriteLine("i与j的值相等");
}

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

本文来自:https://www.cnblogs.com/zhijianliutang/archive/2011/12/06/2278156.html