博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
堆与栈的区别
阅读量:4615 次
发布时间:2019-06-09

本文共 4180 字,大约阅读时间需要 13 分钟。

C#中堆和栈的区别分析
http://www.2cto.com/kf/201109/103302.html
http://www.cnblogs.com/codingsilence/archive/2011/02/28/2146584.html
http://www.cnblogs.com/yplong/p/3467025.html
2011-09-08       0       
    

线程堆栈:简称栈 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所示,

-0----------------------------------------------------

string是引用类型,但它也有一些值类型的特征。 String传值还是传引用 C#的String声明是class String,当然是传引用。 不过,之所以有这个疑惑,多数是因为这个情况: string a = "aaa"; string b = a; b = "bbb"; 或者是这么几行代码: public void Swap(string s1, string s2) {     string temp=s1;     s1=s2;     s2=temp; } 这时候结果一打印,结果发现a的值还没有变,Swap也没有成功,这时候就会有幻觉:是不是没有传引用啊? 呵呵,string不会这么粗暴的打乱“声明为class就是传引用”这种规则的。 分析一下: string a = "aaa"; //==> a----->new String("aaa") string b = a;        //==> b----->a, 传引用 b = "bbb";          //==> b----->new String("bbb"), 传引用,b指向了一个新的字符串,a并没有变。 Swap函数也是这样,比如说传了a, b进去(a="aaa", b="bbb"),     //s1----->a, s2----->b     string temp=s1;//temp----->s1----->a     s1=s2;               //s1----->s2----->b;     s2=temp;          //s2----->temp----->a 结果是,s1和s2确实是Swap了,但是这种结果并不会影响到a和b

转载于:https://www.cnblogs.com/chengjun/p/4451608.html

你可能感兴趣的文章
【洛谷1829】 [国家集训队] Crash的数字表格(重拾莫比乌斯反演)
查看>>
[转]免费api大全
查看>>
git 认证问题之一的解决 : http ssh 互换
查看>>
sql where 1=1作用
查看>>
搜索算法----二分查找
查看>>
Python语言编程
查看>>
[poj 1469]Courses
查看>>
Xcode8出现AQDefaultDevice(173):Skipping input stram 0 0 0x0
查看>>
数据结构(二十四)二叉树的链式存储结构(二叉链表)
查看>>
Material Design Lite,简洁惊艳的前端工具箱 之 布局组件。
查看>>
Django----------路由控制
查看>>
将数字转化为字符串的快捷方式
查看>>
java23种设计模式
查看>>
优化算法与特征缩放
查看>>
NOIP模板复习(4)区间操作之莫队算法,树状数组,线段树
查看>>
深入理解PHP中的引用和赋值
查看>>
compilation与编译
查看>>
vue+element-ui实现表格checkbox单选
查看>>
select * 和select 1 以及 select count(*) 和select count(1)的区别
查看>>
进度条04
查看>>