c#内存管理
尽管在.net framework中我们不太需要关注内存管理和垃圾回收这方面的问题,但是出于提高我们应用程序性能的目的,在我们的脑子里还是需要有这方面的意识。明白内存管理的基本行为将有助于我们解释我们程序中变量是如何操作的。在本文中我将讨论栈和堆的一些基本知识,变量的类型和某些变量的工作原理。
当你在执行程序的时候内存中有两个地方用于存储程序变量。如果你还不知道,那么就来看看堆和栈的概念。堆和栈都是用于帮助我们程序运行的,包含某些特殊信息的操作系统内存模块。那么堆和栈有什么不同呢?
堆VS栈的区别
栈主要用于存储代码,自动变量等信息;而堆则主要用于存储运行期生成的对象等信息。将栈看作是一个有着层级关系的盒子,我们每一次只能操作盒子最上一格的东西。这也就是栈先进后出的数据结构特性。因此栈在我们程序中主要是用于保存程序运行时的一些状态信息。堆则主要是用于保存对象内容,以便我们能够在任何时候去访问这些对象。总的来说,堆就是一种数据结构,我们不需要通过一套规则,可以随时访问的内存区域;栈则总是依据先进后出的,每次只能访问最顶层元素的内存区域。下面是个示意图:
由于栈的特性所至,所以栈具有自我维护性,栈的内存管理可以通过操作系统来完成。而堆的管理就需要通过GC(垃圾回收器)来完成,使用一定的算法来扫描并释放没有用的对象。
关于栈和堆的更多内容
我们代码中有四种主要的类型需要存储在栈和堆当中:值类型,引用类型,指针和程序指令。
值类型:
在c#中主要的值类型有:
bool ,byte ,char ,decimal ,double ,enum ,float ,int ,long ,sbyte ,short ,struct ,uint ,ulong ,ushort都来自于System.TypeValue。
引用类型:
在C#中主要的引用类型有:
class, interface, delegate,object,string所有的引用类型都继承自System.Object。
指针:
在我们的内存管理中一个指针的意义就是一个引用对应到一个类型上。在.net framework中我们不能显式的使用指针,所有的指针都被通用语言运行时(CLR)管理。指针是一块指向其他内存区域的内存区域。指针需要占据一定的内存空间就像其他任何数据一样。
指令:
指令就是计算机执行代码,如函数调用或是数据运算等。
内容和地址的问题
首先有两点需要说明:
1. 引用类型总是存在于堆里 – 很简单,但是完全正确吗?
2. 值类型和指针总是出现在他们声明的地方。这个有点复杂需要相关的栈工作原理的知识。
栈就像我们之前提到的那样,记录我们程序执行时的一些信息。当我们在调用一个类的方法时,操作系统将调用指令压栈并附带方法参数。然后进入函数体处理变量操作。这个可以用下面的代码来解释:
public int AddFive(int pValue) {
int result;
result = pValue + 5;
return result;
}
这个操作发生在栈的顶部,请注意我们看到已经有很多成员之前被压入到栈中了。首先是方法的本身先被压入栈中,紧接着是参数入栈。
然后是通过AddFive()里面的指令来执行函数。
函数执行的结果同样也需要分配一些内存来存放,而这些内存也分配在栈中。
函数执行结束后,就要将结果返回。
最后,通过删除AddFive()的指针来清除所有之前栈中有关于函数运行时分配的内存。并继续下一个函数(可能之前就存在在栈中)。
在这个例子中,我们的结果存储在栈中。事实上,所有函数体内的值类型声明都会分配到栈中。但是现在有些值类型也被分配在堆中。记住一个规则,值类型总是出现在声明它们的地方。如果一个值类型声明在函数体外,但是存于一个引用类型内,那么它将跟这个引用类型一样位于堆中。这里用另外的一个例子来说明这个问题:
public class MyInt{
public int MyValue;
}
public MyInt AddFive(int pValue){
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
现在这个函数的执行跟先前的有了点不同。这里的函数返回是一个MyInt类对象,也就是说是一个引用类型。引用类型是被分配在堆中的,而引用的指针是分配在栈中。
在AddFive()函数执行结束后,我们将清理栈中的内存。
在这里我们看到除了栈中有数据,在堆中也有一些数据。而堆中的数据将被垃圾回收器回收。当我们的程序需要一块内存并且已经没有空闲的内存可以分配时,垃圾回收器开始运行。垃圾回收器会先停止所有运行中的线程,扫描堆中的所有对象并删除那些没有被主程序访问的对象。垃圾回收器将重新组织堆中的所有空闲的空间,并调整所有栈中和堆中的相关指针。就像你能想到的那样,这样的操作会非常的影响效率。因此这也是为什么我们要强调编写高性能的代码。好,那我要怎么样去做呢?
当我们在操作一个引用类型的时候,我们操作的是它的指针而不是它本身。当我们使用值类型的时候我们使用的是它本身,这个很明显。我们看一下代码:
public int ReturnValue() {
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
这段代码很简单,返回3。但是如果我们改用引用类型MyInt类,结果可能不同:
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。为什么呢? 想象一下,我们之前讲的内容,我们在操作值类型数据的时候只是操作该值的一个副本。而在操作引用类型数据的时候,我们操作的是该类型的指针,所以y = x就修改了y的指针内容,从而使得y也指向了x那一部分栈空间。所以y.MyValue = 4 => x.MyValue = 4。所以返回值会是4 。
参数
当我们开始调用一个方法的时候,发生了什么呢?
1. 在栈中分配我们方法所需的空间,包括回调的指针空间,该指针通过一条goto指令来回到函数调用开始的那个栈位置的下一个位置,以便继续执行。
2. 我们方法的参数将被拷贝过来。
3. 控制器通过JIT方法和线程开始执行代码,因此我们有了另外一个称呼叫调栈。
代码如下:
public int AddFive(int pValue){
int result;
result = pValue + 5;
return result;
}
栈的结构模式:
参数在栈中的位置取决于它的类型,值类型本身被拷贝而引用类型的引用被拷贝。
传递值类型参数
当我们传递一个值类型参数时,内存先被分配然后是值被拷贝到栈中。代码如下:
class Class1 {
public void Go () {
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive (int pValue) {
pValue += 5;
return pValue;
}
}
AddFive方法被执行,x位置变成5
当AddFive()方法执行结束后,线程回到执行go方法,pValue将被删除。
所以当我们在传递一个很大的值类型的时候,程序会逐位的拷贝到栈中,这很明显就是效率很低。更何况我们的程序如果要传递这个值数千次的进行,那么效率就更低。
这时我们就要用到引用类型来解决这样的问题。
public void Go() {
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct {
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue) {
// DO SOMETHING HERE....
}
这种方法就更有效的进行操作内存,其实我们并不需要拷贝这块内存。
当我们传递的是值类型的引用,那么程序修改这个引用的内容都会直接反映到这个值上。
传递引用类型
传递引用类型参数有点类似于前面的传递值类型的引用。
public class MyInt {
public int MyValue;
}
public void Go() {
MyInt x = new MyInt();
x.MyValue = 2;
DoSomething(x);
Console.WriteLine(x.MyValue.ToString());
}
public void DoSomething(MyInt pValue) {
pValue.MyValue = 12345;
}
这段代码做了如下工作:
1. 开始调用go()方法让x变量进栈。
2.