一、在讲堆栈之前,我们先看看值类型和引用类型:
1,我们看看值类型与引用类型的存储方式:
引用类型:引用类型存储在堆中。类型实例化的时候,会在堆中开辟一部分空间存储类的实例。类对象的引用还是存储在栈中。
值类型:值类型总是分配在它声明的地方,做为局部变量时,存储在栈上;类对象的字段时,则跟随此类存储在堆中。
什么是堆什么是栈我们后面解释。
图1-1
2,我们再看看引用类型与值类型的区别:
①引用类型和值类型都继承自Systerm.Object类。不同之处,几乎所有的引用类型都是直接从Systerm.Object继承,而值类型则是继承Systerm.Object的子类Systerm.ValueType类。
②我们在给引用类型的变量赋值的时候,其实只是赋值了对象的引用;而给值类型变量赋值的时候是创建了一个副本(副本不明白?说通俗点,就是克隆了一个变量)。
文字不够形象?我们上代码看看
图1-2
3,我们再看看引用类型和值类型的内存分配情况(我们对着代码与图看)
图1-3
图1-4
看了图1-3和图1-4之后,你们可能问我了:你是怎么知道变量在栈中的地址的。嘿嘿,等下教你用c#玩指针
从上面两张图我们可以看出:
①栈的结构是后进先出,也就是说:变量j的生命周期在变量s之前结束,变量s的生命周期在变量i之前结束,
②栈地址从高往底分配
③类型的引用也存储在栈中
线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap
使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。如果我们写出如下两段代码:
public int AddFive(int pValue) { int result; result = pValue + 5; return result; } 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,译注:调用帧),接着下一个。
栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。
堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
如图1,栈与堆示意图
二,引用和值类型如何分配?
我们先看一下两个观点:
观点1,引用类型总是被分配在堆上。(正确?)
观点2,值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上。
上文提及的栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。
下面我们以实例来详谈。
还是我们开篇所列的代码段1:
public int AddFive(int pValue) { int result; result = pValue + 5; return result; }
当AddFive方法开始执行的时候,方法参数(parameters)则在栈上分配。如图3:
注意:方法并不在栈中存活,图示仅供参考。
接着,指令指向AddFive方法内部,如果该方法是第一次执行,首先要进行JIT即时编译。如图4:
方法执行完毕,而且方法返回后,如图6所示:
在方法执行完毕返回后,栈上的区域被清理。如图7:
现在可以回答第一个问题了 : 很明显 pValue 与 result 都是被分配在stack上的,而且生命周期为这个函数的生命周期
以上看出,一个值类型变量,一般会分配在栈上。那观点2中所述又做何理解?“值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上”。
原因就是如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。
还是代码段2
public
class
MyInt
{
public
int
MyValue;
}
public
MyInt AddFive(
int
pValue)
{
MyInt result =
new
MyInt();
result.MyValue = pValue + 5;
return
result;
}
刚开是的时候和代码段一是一样的,先找到方法,然后定义参数 pValue
接下来就不一样了,要定义一个引用类型
1
|
MyInt result = new MyInt(); |
new Myint()将会出现在托管堆中,而result被定义在堆栈中,其内容是指向 new MyInt()的地址,如下如所示
AddFive方法执行完毕后 stack将被清理,而heap将会被保留一段时间,这一段时间示情况而定,如果没有任何引用指向MyInt 垃圾管理器将会在合适的时候(不确定的时间)处理它
这样就能很好的回答问题一了
值类型嵌套引用类型 : 和代码段二的理解方式一致,值类型将会被分配在stack上,二其内部的引用将会在heap上被声明.
引用类型嵌套值类型:如上图的邮编堆,都会被声明在heap上