栈和堆
C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成我们C#程序的运行环境。
一,栈 vs 堆:区别?
栈通常保存着我们代码执行的步骤,如在代码段1中 AddFive()方法,int pValue变量,int result变量等等。而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉
栈内存无需我们管理,也不受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),它所指向的另一块内存区才是我们真正的数据或者类型。
指令:
后文对指令再做介绍。
三,如何分配?
我们先看一下两个观点:
观点1,引用类型总是被分配在堆上。(正确?)
观点2,值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上。(这个理解起来有点难度,需要慢慢来)
上文提及的栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。
下面我们以实例来详谈。
还是我们开篇所列的代码段1:
1 public int AddFive(int pValue) 2 { 3 int result; 4 result = pValue + 5; 5 return result; 6 }
当AddFive方法开始执行的时候,方法参数(parameters)则在栈上分配。
接着,指令指向AddFive方法内部,如果该方法是第一次执行,首先要进行JIT即时编译。
当方法内部开始执行的时候,变量result被分配在栈上,方法执行完毕,而且方法返回后,栈上的区域被清理。
以上看出,一个值类型变量,一般会分配在栈上。那观点2中所述又做何理解?“值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上”。
原因就是如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。
还是代码段2:
1 public class MyInt 2 { 3 public int MyValue; 4 } 5 6 public MyInt AddFive(int pValue) 7 { 8 MyInt result = new MyInt(); 9 result.MyValue = pValue + 5; 10 return result; 11 }
当线程开始执行AddFive方法的时候,参数被分配到栈上,由于MyInt是一个引用类型,所以它被分配到堆上,并且在栈中生成一个指针(result),AddFive方法执行完毕时,栈上内存被清理,堆中依然存在。
当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有不可达到对象,即无被引用的对象,进行清理。并通知栈中的指针重新指向地址排序后的对象。现在我们应该知道,了解栈和堆,对我们开发出高性能程序的重要性。当我们使用引用类型的时候,一般是对指针进行的操作而非引用类型对象本身。但是值类型则操作其本身。
接下来,我们用例子说明这一点。
1 例1: 2 3 public int ReturnValue() 4 { 5 int x = new int(); 6 x = 3; 7 int y = new int(); 8 y = x; 9 y = 4; 10 return x; 11 }
执行结果为3,稍作修改:
1 例2: 2 3 public class MyInt 4 { 5 public int MyValue; 6 } 7 8 public int ReturnValue2() 9 { 10 MyInt x = new MyInt(); 11 x.MyValue = 3; 12 MyInt y = new MyInt(); 13 y = x; 14 y.MyValue = 4; 15 return x.MyValue; 16 }
执行结果为4。
我们来分析下原因,其实例1的跟以下代码所起效用一样:
1 public int ReturnValue() 2 { 3 int x = 3; 4 int y = x; 5 y = 4; 6 return x; 7 }
在栈上x和y分别占用一块内存区,互不干扰。
而例2,与以下代码所起效用一样:
1 public int ReturnValue2() 2 { 3 MyInt x; 4 x.MyValue = 3; 5 MyInt y; 6 y = x; 7 y.MyValue = 4; 8 return x.MyValue; 9 }
C#中栈和堆怎么定义
栈当中存放的是值类型,如int,decimal,double,enum等
堆当中存放的是引用类型,如string,类等
如图,如果定义一个int类型的变量i:
1
|
int i = 5; |
那么在内存中的实际情况是:直接在栈中存放i的值5.
如果顶一个string类型的变量s:
1
|
string s = "A string." ; |
那么在内存中的实际情况是:在堆中存放s的变量名(即地址),在栈中存放s的值"A string.",然后让堆中的变量名s(即地址)指向栈中的值,这也是为什么,保存在堆中的变量被称为引用类型,因为当你访问一个引用类型的变量时,首先是访问它的地址,然后通过引用才能访问到该变量的值。