C# 堆(Heap) Vs 栈 (Stack) in .NET (1/4)

 .NET framework使我们不需要刻意关心内存管理和垃圾回收(GC),但是当我们需要优化应用的性能的时,我们就需要对他们有所了解。理解内存解能帮助我们知道我们所写编码中变量的行为,在这篇文章中我将描述 堆和栈的基本知识, 变量和变量的工作原理。

 

当执行程序时 .NET framework有俩个地方来存放数据,他们就是堆 (Heap)和栈 (Stack),他们位于我们机器的内存里保存我们程序运行过程中的数据。

 

堆 (Heap) Vs 栈 (Stack) 不同之处?

 

栈 (Stack) 负责记录线程的运行运行到哪里(或者什么正在被调用)
堆 (Heap)负责保存对象,数据...

 

我们可以把栈(Stack)想象为从上到下叠放在一起的盒子,我每次调用一个方法时就相当于在这些盒上放一个新的盒子,我们通过记录这个新的盒子来表示程序正在干什么 (或者说运行到哪里), 我们只能使用最顶端的盒子,当最顶端的盒子被使用完成后(被执行的方法执行完毕),我们就把这个盒子拿掉,接着处理紧挨着被拿掉盒子下面的第一个盒子(也即当前最顶端的盒子),堆也是很相似除了他的目的是用来保存信息(大部分时间它不记录执行状态)所以堆上的任何东西都是可以被随时访问的,它不像栈那样对访问其上面的东西有限制,堆就像是一堆我们洗干净放在床上还没有整理的衣服,我们可以很快找到我们要穿的那件衣服,栈就像是壁橱里面叠在一起的鞋盒,如果我们想拿第二个盒子那我们就必须先把第一个拿出来。

 

 

 

上面的图片不能真正的反应堆和栈在内存的状态,只是用来帮助我们区分和理解它们。

 

栈是自我管理的,也就是说他是自己管理自己的内存,当第一层的盒子不需要再被使用了,那么这个盒子就会被拿掉,堆却相反,他必须考虑垃圾回收(GC),使堆保持干净 (没有人希望床上都是脏兮兮的衣服,它让人作恶)

 

堆和栈上都是什么?

有四种主要的类型将会被放到堆和栈上当代码运行时:值类型,引用类型,指针,和指令。

 

值类型:

在C# 中所有被下面类型声明的变量都是值类型的 (因为这些类型来自 System.ValueType):
  • bool
  • byte
  • char
  • decimal
  • double
  • enum
  • float
  • int
  • long
  • sbyte
  • short
  • struct
  • uint
  • ulong
  • ushort
引用类型:

所有被下面类型声明的类型 (继承System.Object... 还有那些它本身就是System.Object 的对象)
  • class
  • interface
  • delegate
  • object
  • string
指针:

第三种类型是作为一个类型的引用引入到我们的内存管理的模式中,一个引用通常也被称作指针,我们并没有明确的使用指针,他们由CLR管理。
指针和引用类型是不同的,我们说一个东西是引用类型也就是说我们可以通过指针来访问它,一个指针就是一块内存空间它指向内存中的另一个地方,
指针像其他类型一样可以被放到堆和栈中并占用内存空间,他的值可以使一个内存地址或者null.

 

指令:

 

我们将在后续的文章中介绍它。

那么它们是怎么样被分配到堆和栈上面的?

好的,这是最后一个比较有意思的事情。
这里有俩个黄金法则:
1,引用类型肯定是放在堆上, 简单吧?:)
2,值类型和指针类型由他们在哪里声明决定,这个有点复杂,我们需要理解栈是怎么工作的,然后才能指出他们是在哪里声明的。
栈,如我们开始所说,它负责记录每个线程执行我们的代码到了那个位置(或者什么正在被执行), 你可以把它理解为线程的“状态”,并且每个线程有自己的栈
当我们的代码需要去执行一个方法时,线程就会去执行已经被 JIT编译好的并且位于方法表中的一系列指令,它同时也会把方法的参数分配到栈栈顶上,
然后他就会执行位于栈顶的代码用属于这个方法的变量,我们可以通过一个例子来理解它...
我们来看下面的方法。

 

1 public int AddFive(int pValue)
2 {
3   int result;
4   result = pValue + 5;
5   return result;
6 }
 

下面就是在栈顶发生的事情,请注意当我们在关注栈顶的时候其他的一些相关的东西已经在放到栈中

一旦我们开始执行方法,栈上就会分配该方法的参数(我们将会在后面讨论怎么传递参数) 

 

注意,方法不是居于栈中,它只供我们描述参考

 

 

 

接下来,控制开始执行AddFive() 位于类型方法表中的指令,如果是第一次执行这个方法,JIT就会编译这些指令。

 

 

 

随方法的执行,我们需要在栈分配内存给“result”变量。

 

 

 

方法执行结束,我们的结果返回。

 

 

 

然后栈向下移动指针到这个方法下面的方法,释放分配给这个方法的所有资源。

 

 

 

在这个示例中, 我们的 "result" 变量是分配在栈上面的,实际上,方法内部的所有值类型变量都是分配在栈上的。
现在 值类型有时也会被分配到堆上面,记住,值类型都是在哪里声明就会被分配到哪里。 如果一个值类型在方法体外面定义,但是却在一个引用类型内部,它将会被分配到引用类型内的堆上面。

 

我们来看下面一个例子。

 

我们有下面一个 MyInt 类 (一个引用类型,因为它是一个类)
1  public class MyInt
2 { 
3  public int MyValue;
4 }
 

然后下面的方法被执行:

 

1 public MyInt AddFive(int pValue)
2 {
3  MyInt result = new MyInt();
4  result.MyValue = pValue + 5;
5  return result;
6 }
 

如刚才一样,线程开始执行这个方法,方法的参数也被分配到线程的栈上

 

 

现在事情变得有意思了...
因为MyInt 是一个引用类型,它被分配到堆上然后被一个栈上面的指针引用

 

 

 

当 AddFive() 被执行完后(就像我们的第一个例子一样)我们开始清理...

 

 

 

然后只剩下一个孤独的MyInt在堆上面(栈上没有任何指针指向它(MyInt))

 

 

 

现在垃圾回收(GC)就出场了,一旦我们的程序运行到一个临界点并且我们需要更多的堆空间,我们的GC就开始运作,它会停止所有的线程,清理所有堆上面的没有被程序使用的对象,然后删除它们,重新组织所有剩下的对象来腾出空间并调整所有对象在堆和栈上的指针,显然这对性能有很大的影响,所以我们知道理解堆和栈是什么对我们写出高性能的代码是多么重要。

 

那它是怎样影响我们编程的啦?

 

当我们使用引用类型时,我们要处理指向这些类型的指针,而不是这些类型。
当我们使用值类型,我们真正的在使用它们。太简单了,不是吗?

 

我们再用例子来解释下吧,
如果我们执行下面的方法:
1 public int ReturnValue()
2 {
3  int x = new int();
4  x = 3;
5  int y = new int();
6  y = x; 
7  y = 4
8  return x;
9 }
我们得到值类型 3,太简单了。 但是如果我们使用先前的MyInt 类啦?
1 public int ReturnValue2()
2 {
3  MyInt x = new MyInt();
4  x.MyValue = 3;
5  MyInt y = new MyInt();
6  y = x; 
7  y.MyValue = 4
8  return x.MyValue;
9 }

那我们的结果是? 4!
这是为什么啦? x.MyValue是怎样变成4的?
在上面第一个例子中它的结果是按我们预想的执行的
1 public int ReturnValue()
2 {
3  int x = 3;
4  int y = x; 
5  y = 4;
6  return x;
7 }

但是在下面一个例子中,我们没有获得 “3” 是因为 变量 "x" 和 "y" 都是指向堆上的同一个变量
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 }
 

希望这写能让你对值类型和引用类型有一个更好的理解,知道什么是指针和它们是什么时候使用的,在接下来的文章中我将会深入到内存管理和方法的参数。


 

posted on 2012-05-10 17:40  Simon.Huang  阅读(2091)  评论(7编辑  收藏  举报

导航