浅谈C#中堆和栈的区别(附上图解)

线程堆栈:简称栈 Stack

托管堆: 简称堆 Heap

使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。如果我们写出如下两段代码:

代码段1:

public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}

代码段2:

public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}

问题1:你知道代码段1在执行的时候,pValue和result在内存中是如何存放,生命周期又如何?代码段2呢?

要想释疑以上问题,我们就应该对.Net下的栈(Stack)和托管堆(Heap)(简称堆)有个清楚认识,本立而道生。如果你想提高程序性能,理解栈和堆,必须的!

本文就从栈和堆,类型变量展开,对我们写的程序进行庖丁解牛。

C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成我们C#程序的运行环境。


一,栈 vs 堆:区别?

栈通常保存着我们代码执行的步骤,如在代码段1中 AddFive()方法,int pValue变量,int result变量等等。而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。

如图1,栈与堆示意图

(图1)

栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。


二,什么元素被分配到栈?什么被分配到堆?

当我们程序执行的时候,在栈和堆中分配有四种主要的类型:值类型,引用类型,指针,指令。

值类型:

在C#中,继承自System.ValueType的类型被称为值类型,主要有以下几种(CLR2.0中支持类型有增加):

* bool

* byte

* char

* decimal

* double

* enum

* float

* int

* long

* sbyte

* short

* struct

* uint

* ulong

* ushort

引用类型:

以下是引用类型,继承自System.Object:

* class

* interface

* delegate

* object

* string

指针:

在内存区中,指向一个类型的引用,通常被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显示使用。需要注意的是,一个类型的引用即指针跟引用类型是两个完全不同的概念。指针在内存中占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区才是我们真正的数据或者类型。如图2:

(图2)

指令:

后文对指令再做介绍。


三,如何分配?

我们先看一下两个观点:

观点1,引用类型总是被分配在堆上。(正确?)

观点2,值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上。(这个理解起来有点难度,需要慢慢来)

上文提及的栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。

当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。

下面我们以实例来详谈。

还是我们开篇所列的代码段1:

public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}

当AddFive方法开始执行的时候,方法参数(parameters)则在栈上分配。如图3:

(图3)

注意:方法并不在栈中存活,图示仅供参考。

接着,指令指向AddFive方法内部,如果该方法是第一次执行,首先要进行JIT即时编译。如图4:

(图4)

当方法内部开始执行的时候,变量result被分配在栈上,如图5:

(图5)

方法执行完毕,而且方法返回后,如图6所示:

(图6)

在方法执行完毕返回后,栈上的区域被清理。如图7:

(图7)

以上看出,一个值类型变量,一般会分配在栈上。那观点2中所述又做何理解?“值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上”。

原因就是如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。

还是代码段2:

public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}

当线程开始执行AddFive方法的时候,参数被分配到栈上,如图8所示:

(图8)

由于MyInt是一个引用类型,所以它被分配到堆上,并且在栈中生成一个指针(result),如图9:

(图9)

AddFive方法执行完毕时的情况如图10:

(图10)

栈上内存被清理,堆中依然存在,如图11:

(图11)

当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有不可达到对象,即无被引用的对象,进行清理。并通知栈中的指针重新指向地址排序后的对象。现在我们应该知道,了解栈和堆,对我们开发出高性能程序的重要性。当我们使用引用类型的时候,一般是对指针进行的操作而非引用类型对象本身。但是值类型则操作其本身。

接下来,我们用例子说明这一点。

例1:

public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}

执行结果为3,稍作修改:

例2:

public class MyInt
{
public int MyValue;
}
public int ReturnValue2()
{
MyInt x = new MyInt();
x.MyValue = 3;
MyInt y = new MyInt();
y = x;
y.MyValue = 4;
return x.MyValue;
}

执行结果为4。

我们来分析下原因,其实例1的跟以下代码所起效用一样:

public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}

如图12所示,在栈上x和y分别占用一块内存区,互不干扰。

(图12)

而例2,与以下代码所起效用一样:

public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}

如图13所示,

(图13)

栈上的指针x和y指向堆上同一个区域,修改其一必会改变堆上的数据。

除声明外,跑步客文章均为原创,转载请以链接形式标明本文地址
  浅谈C#中堆和栈的区别(附上图解)

本文地址:  http://www.paobuke.com/develop/c-develop/pbk23496.html

相关内容

C#文件上传与下载的实现方法

C#字符串自增自减算法详解

C#实现Excel表数据导入Sql Server数据库中的方法

C#实现判断文件夹存在与否并创建文件夹的方法


如何使用C#读写锁ReaderWriterLockSlim

C#图像颜色聚类高效方法实例

C#选择排序法实例分析

C#使用委托的步骤浅析

时间: 2024-08-25 07:32:00

浅谈C#中堆和栈的区别(附上图解)的相关文章

转 浅谈C++中指针和引用的区别

浅谈C++中指针和引用的区别 浅谈C++中指针和引用的区别 指针和引用在C++中很常用,但是对于它们之间的区别很多初学者都不是太熟悉,下面来谈谈他们2者之间的区别和用法. 1.指针和引用的定义和性质区别: (1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已.如: int a=1;int *p=&a; int a=1;int &b=a; 上面定义了一个整形变量和一个指针变量p,该指针变量指向a

C语言中堆和栈的区别

原文:http://blog.csdn.net/tigerjibo/article/details/7423728 一.前言: C语言程序经过编译连接后形成编译.连接后形成的二进制映像文件由栈,堆,数据段(由三部分部分组成:只读数据段,已经初始化读写数据段,未初始化数据段即BBS)和代码段组成,如下图所示: 1.栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值.其操作方式类似于数据结构中的栈. 2.堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄

iOS中堆和栈的区别

堆和栈的区别主要有以下五点: 1.管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制:对于堆来讲,释放工作由程序员控制,容易产生memory leak(内存泄露). 2.申请大小: 栈是向低地址扩展的数据结构,是一块连续的内存区域.这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,是一个编译时就确定的常数,如果申请空间超过栈的剩余空间时,将提示overflow(溢出).因此,能从栈获得的空间较小. 堆是向高地址扩展的数据结构,是不连续的内存区域.这是由于系统是由链表来存储的空闲内

浅谈Java中set.map.List的区别

就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操作数目不固定的一组数据. 所有的JAVA集合都位于 java.util包中! JAVA集合只能存放引用类型的的数据,不能存放基本数据类型. JAVA集合主要分为三种类型: Set(集) List(列表) Map(映射) Collection 接口 :Collection是最基本的集合接口,声明了适用

java中堆与栈的区别

堆与栈都是java中常用的存储结构,是内存中存放数据的地方. 堆:主要存放引用类型的变量,以及运行时创建(new)的对象.主要用于储存对象,存取速度慢,可以运行时动态分配内存,生命周期不需要提前确定. 栈:主要存放基础类型的变量,以及对象的引用变量.主要用于执行程序,存取速度快,生命周期必须要提前确定,缺少灵活性. public class Test{ public static void main(String[] args){ int i=0; //基本数据类型变量 鸭子 duck =new

多线程中 堆和栈的区别

很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G.   一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack),Windows线程的缺省堆栈大小为1M.堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,Windows进程还有所谓进程默认堆,用户也可以创建自己的堆. 堆: 是大家共有的空间,分全局堆和局

内存中堆,栈的区别

原文链接:https://blog.csdn.net/tiger406/article/details/1192110 五大内存分区 看图: 在C++中,内存分成5个区,他们分别是堆.栈.自由存储区.全局/静态存储区和常量存储区. 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.    堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete.如果程序员没有释放掉,那

浅谈HTTP中Get与Post的区别_转

可参考:HTTP请求中POST与GET的区别 Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE.URL全称是资源描述符,我们可以这样认为:一个URL地址,它用于描述一个网络上的资源,而HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查,改,增,删4个操作.到这里,大家应该有个大概的了解了,GET一般用于获取/查询资源信息,而POST一般用于更新资源信息. 1.根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的

java中堆和栈的区别

Java的堆是一个运行时数据区,类的(对象从中分配空间.这些对象通过new.newarray.anewarray和multianewarray等 指令建立,它们不需要程序代码来显式的释放.堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时 动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据.但缺点是,由于要在运行时动态分配内存,存取速度较慢. 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享.但缺点是,存在栈中的数据大小