.Net中,栈和堆的区别(转)

尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们 日常程序编写中的变量的行为。在本文中我将讲解栈和堆的基本知识,变量类型以及为什么一些变量能够按照它们自己的方式工作。

在.NET framework环境下,当我们的代码执行时,内存中有两个地方用来存储这些代码。假如你不曾了解,那就让我来给你介绍栈(Stack)和堆 (Heap)。栈和堆都用来帮助我们运行代码的,它们驻留在机器内存中,且包含所有代码执行所需要的信息。


* 栈vs堆:有什么不同?

栈负责保存我们的代码执行(或调用)路径,而堆则负责保存对象(或者说数据,接下来将谈到很多关于堆的问题)的路径。

可以将栈想象成一堆从顶向下堆叠的盒子。当每调用一次方法时,我们将应用程序中所要发生的事情记录在栈顶的一个盒子中,而我们每次只能够使用栈顶的 那个盒子。当我们栈顶的盒子被使用完之后,或者说方法执行完毕之后,我们将抛开这个盒子然后继续使用栈顶上的新盒子。堆的工作原理比较相似,但大多数时候 堆用作保存信息而非保存执行路径,因此堆能够在任意时间被访问。与栈相比堆没有任何访问限制,堆就像床上的旧衣服,我们并没有花时间去整理,那是因为可以 随时找到一件我们需要的衣服,而栈就像储物柜里堆叠的鞋盒,我们只能从最顶层的盒子开始取,直到发现那只合适的。

[heapvsstack1.gif]

以上图片并不是内存中真实的表现形式,但能够帮助我们区分栈和堆。

栈是自行维护的,也就是说内存自动维护栈,当栈顶的盒子不再被使用,它将被抛出。相反的,堆需要考虑垃圾回收,垃圾回收用于保持堆的整洁性,没有人愿意看到周围都是赃衣服,那简直太臭了!


* 栈和堆里有些什么?

当我们的代码执行的时候,栈和堆中主要放置了四种类型的数据:值类型(Value Type),引用类型(Reference Type),指针(Pointer),指令(Instruction)。

1.值类型:

在C#中,所有被声明为以下类型的事物被称为值类型:

bool
byte
char
decimal
double
enum
float
int
long
sbyte
short
struct
uint
ulong
ushort


2.引用类型:

所有的被声明为以下类型的事物被称为引用类型:

class
interface
delegate
object
string


3.指针:

在内存管理方案中放置的第三种类型是类型引用,引用通常就是一个指针。我们不会显示的使用指针,它们由公共语言运行时(CLR)来管理。指针(或引 用)是不同于引用类型的,是因为当我们说某个事物是一个引用类型时就意味着我们是通过指针来访问它的。指针是一块内存空间,而它指向另一个内存空间。就像 栈和堆一样,指针也同样要占用内存空间,但它的值是一个内存地址或者为空。

[heapvsstack2.gif]

4.指令:

在后面的文章中你会看到指令是如何工作的...

* 如何决定放哪儿?


这里有一条黄金规则:

1. 引用类型总是放在堆中。(够简单的吧?)

2. 值类型和指针总是放在它们被声明的地方。(这条稍微复杂点,需要知道栈是如何工作的,然后才能断定是在哪儿被声明的。)

就像我们先前提到的,栈是负责保存我们的代码执行(或调用)时的路径。当我们的代码开始调用一个方法时,将放置一段编码指令(在方法中)到栈上,紧接着放置方法的参数,然后代码执行到方法中的被“压栈”至栈顶的变量位置。通过以下例子很容易理解...

下面是一个方法(Method):

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

现在就来看看在栈顶发生了些什么,记住我们所观察的栈顶下实际已经压入了许多别的内容。

首先方法(只包含需要执行的逻辑字节,即执行该方法的指令,而非方法体内的数据)入栈,紧接着是方法的参数入栈。(我们将在后面讨论更多的参数传递)

[heapvsstack3.gif]

接着,控制(即执行方法的线程)被传递到堆栈中AddFive()的指令上,

[heapvsstack4.gif]

当方法执行时,我们需要在栈上为“result”变量分配一些内存,

[heapvsstack5.gif]

The method finishes execution and our result is returned.
方法执行完成,然后方法的结果被返回。

[heapvsstack6.gif]

通过将栈指针指向AddFive()方法曾使用的可用的内存地址,所有在栈上的该方法所使用内存都被清空,且程序将自动回到栈上最初的方法调用的位置(在本例中不会看到)。

[heapvsstack7.gif]


在这个例子中,我们的"result"变量是被放置在栈上的,事实上,当值类型数据在方法体中被声明时,它们都是被放置在栈上的。

值类型数据有时也被放置在堆上。记住这条规则--值类型总是放在它们被声明的地方。好的,如果一个值类型数据在方法体外被声明,且存在于一个引用类型中,那么它将被堆中的引用类型所取代。


来看另一个例子:

假如我们有这样一个MyInt类(它是引用类型因为它是一个类类型):

          public class MyInt
          {         
             public int MyValue;
          }

然后执行下面的方法:

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

就像前面提到的,方法及方法的参数被放置到栈上,接下来,控制被传递到堆栈中AddFive()的指令上。

[heapvsstack8.gif]

接着会出现一些有趣的现象...

因为"MyInt"是一个引用类型,它将被放置在堆上,同时在栈上生成一个指向这个堆的指针引用。

[heapvsstack9.gif]

在AddFive()方法被执行之后,我们将清空...

[heapvsstack10.gif]

我们将剩下孤独的MyInt对象在堆中(栈中将不会存在任何指向MyInt对象的指针!)

[heapvsstack11.gif]

这就是垃圾回收器(后简称GC)起作用的地方。当我们的程序达到了一个特定的内存阀值,我们需要更多的堆空间的时候,GC开始起作用。GC将停止所 有正在运行的线程,找出在堆中存在的所有不再被主程序访问的对象,并删除它们。然后GC会重新组织堆中所有剩下的对象来节省空间,并调整栈和堆中所有与这 些对象相关的指针。你肯定会想到这个过程非常耗费性能,所以这时你就会知道为什么我们需要如此重视栈和堆里有些什么,特别是在需要编写高性能的代码时。

Ok... 这太棒了, 当它是如何影响我的?

Good question. 

当我们使用引用类型时,我们实际是在处理该类型的指针,而非该类型本身。当我们使用值类型时,我们是在使用值类型本身。听起来很迷糊吧?

同样,例子是最好的描述。

假如我们执行以下的方法:

          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!

为什么?...  x.MyValue怎么会变成4了呢?...  看看我们所做的然后就知道是怎么回事了:

在第一例子中,一切都像计划的那样进行着:

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

[heapvsstack12.gif]

在第二个例子中,我们没有得到"3"是因为变量"x"和"y"都同时指向了堆中相同的对象。
          public int ReturnValue2()
          {
                MyInt x;
                x.MyValue = 3;
                MyInt y;
                y = x;               
                y.MyValue = 4;
                return x.MyValue;
          }

[heapvsstack13.gif]

希望以上内容能够使你对C#中的值类型和引用类型的基本区别有一个更好的认识,并且对指针及指针是何时被使用的有一定的基本了解。在系列的下一个部分,我们将深入内存管理并专门讨论方法参数。

 

原文出处:
http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory2B01142006125918PM/csharp_memory2B.ASPx





尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们 日常程序编写中的变量的行为。在本文中我将讲解我们必须要注意的方法传参的行为。

在第一部分里我介绍了栈和堆的基本功能,还介绍到了在程序执行时值类型和引用类型是如何分配的,而且还谈到了指针。

* 参数,大问题

这里有一个代码执行时的详细介绍,我们将深入第一部分出现的方法调用过程...

当我们调用一个方法时,会发生以下的事情:

1.方法执行时,首先在栈上为对象实例中的方法分配空间,然后将方法拷贝到栈上(此时的栈被称为帧),但是该空间中只存放了执行方法的指令,并没有方法内的数据项。
2.方法的调用地址(或者说指针)被放置到栈上,一般来说是一个GOTO指令,使我们能够在方法执行完成之后,知道回到哪个地方继续执行程序。(最好能理解这一点,但并不是必须的,因为这并不会影响我们的编码)
3.方法参数的分配和拷贝是需要空间的,这一点是我们需要进一步注意。
4.控制此时被传递到了帧上,然后线程开始执行我们的代码。因此有另一个方法叫做"调用栈"。

示例代码如下:

          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;
          }
     }

方法Go()被放置到栈上,然后执行,整型变量"x"的值"5"被放置到栈顶空间中。

 

然后AddFive()方法被放置到栈顶上,接着方法的形参值被拷贝到栈顶,且该形参的值就是"x"的拷贝。

 

当AddFive()方法执行完成之后,线程就通过预先放置的指令返回到Go()方法的地址,然后从栈顶依次将变量pValue和方法AddFive()移除掉:

 

所以我们的代码输出的值是"5",对吧?这里的关键之处就在于任何传入方法的值类型参数都是复制拷贝的,所以原始变量中的值是被保留下来而没有被改变的。

必须注意的是,如果我们要将一个非常大的值类型数据(如数据量大的struct类型)入栈,它会占用非常大的内存空间,而且会占有过多的处理器周期 来进行拷贝复制。栈并没有无穷无尽的空间,它就像在水龙头下盛水的杯子,随时可能溢出。struct是一个能够存放大量数据的值类型成员,我们必须小心地 使用。

这里有一个存放大数据类型的struct:

           public struct MyStruct

           {

               long a, b, c, d, e, f, g, h, i, j, k, l, m;

           }

来看看当我们执行了Go()和DoSometing()方法时会发生什么:

          public void Go()

          {

             MyStruct x = new MyStruct();

             DoSomething(x);

          }

           public void DoSomething(MyStruct pValue)
          
           {
          
                    // DO SOMETHING HERE....

           }

 

这将会非常的低效。想象我们要是传递2000次MyStruct,你就会明白程序是怎么瘫痪掉的了。

那么我们应该如何解决这个问题?可以通过下列方式来传递原始值的引用:

          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....

           }

通过这种方式我们能够提高内存中对象分配的效率。

 

唯一需要注意的是,在我们通过引用传递值类型时我们会修改该值类型的值,也就是说pValue值的改变会引起x值的改变。执行以下代码,我们的结果会变成"123456",这是因为pValue实际指向的内存空间与x变量声明的内存空间是一致的。
         
          public void Go()

          {

             MyStruct x = new MyStruct();

             x.a = 5;

             DoSomething(ref x);

             Console.WriteLine(x.a.ToString());

          }

          public void DoSomething(ref MyStruct pValue)

          {

                   pValue.a = 12345;

          }

* 引用类型传递

传递引用类型参数的情况类似于先前例子中通过引用来传递值类型的情况。

如果我们使用引用类型:

           public class MyInt

           {

               public int MyValue;

           }

然后调用Go()方法,MyInt对象将放置在堆上:

          public void Go()

          {

             MyInt x = new MyInt();              

          }

 

如果我们执行下面的Go()方法:

          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()入栈
2.Go()方法中的变量x入栈
3.方法DoSomething()入栈
4.参数pValue入栈
5.x的值(MyInt对象的在栈中的指针地址)被拷贝到pValue中

因此,当我们通过MyInt类型的pValue来改变堆中MyInt对象的MyValue成员值后,接着又使用指向该对象的另一个引用x来获取了其MyValue成员值,得到的值就变成了"12345"。

而更有趣的是,当我们通过引用来传递一个引用类型时,会发生什么?

让我们来检验一下。假如我们有一个"Thing"类和两个继承于"Thing"的"Animal"和"Vegetable" 类:
          
           public class Thing

           {

           }

           public class Animal:Thing

           {

               public int Weight;

           }

           public class Vegetable:Thing

           {

               public int Length;

           }

然后执行下面的Go()方法:

          public void Go()

          {

             Thing x = new Animal();

             Switcharoo(ref x);

              Console.WriteLine(

                "x is Animal    :   "

                + (x is Animal).ToString());

              Console.WriteLine(

                  "x is Vegetable :   "

                  + (x is Vegetable).ToString());

          }

           public void Switcharoo(ref Thing pValue)

           {

               pValue = new Vegetable();

           }

变量x被返回为Vegetable类型。

x is Animal    :   False
x is Vegetable :   True

让我们来看看发生了什么:

 

1.Go()方法入栈
2.x指针入栈
3.Animal对象实例化到堆中
4.Switcharoo()方法入栈
5.pValue入栈且指向x

 

6.Vegetable对象实例化到堆中
7.x的值通过被指向Vegetable对象地址的pValue值所改变。


如果我们不使用Thing的引用,相反的,我们得到结果变量x将会是Animal类型的。

如果以上代码对你来说没有什么意义,那么请继续看看我的文章中关于引用变量的介绍,这样能够对引用类型的变量是如何工作的会有一个更好的理解。

我们看到了内存是怎样处理参数传递的,在系列的下一部分中,我们将看看栈中的引用变量发生了些什么,然后考虑当我们拷贝对象时是如何来解决某些问题的。

posted @ 2010-06-14 15:25  日新月异  阅读(458)  评论(3编辑  收藏  举报