在.NET中或许我们不用担心内存管理以及垃圾回收器(Garbage Collection GC)的问题,但是我们还是应该了解这些东东以便在必要的时候优化我们程序的性能。而且,如果对内存管理如何工作有所了解,那将有助于解释我们每个程序里的每个变量的运行规律。这篇文章主要内容是解释堆(Heap)和栈(Stack),各种变量以及这些变量到底是如何工作的。

  .Net Framework 在执行代码时,有两个用来存储对象的地方,也就是堆和栈,用于帮助执行我们的代码。它们驻留在机器内存中,包含了所有我们需要实现的信息。

  Stack VS Heap

  栈多多少少用来负责跟踪你的代码里正在执行什么,或者说代码里的什么东东被called。而堆则或多或少用来跟踪我们的对象,或者说数据,大多数情况下都是数据啦——后头再详解。

  把栈想象成堆砌起来由上到下的盒子。每次我们调用一个
















上图虽然并不是真正的内存中堆栈的样子,不过有助于我们理解它们的区别。

  栈是“自我维护”的,意思是基本上是管理自个儿的(而不是别的地方的)内存。当顶部盒子不在使用,就扔之(就不是自家的雪了)!而堆呢,不太一样的是,必须得跟GC打交道——这东西用来保证堆是clean(没有过多垃圾内存)的。(木有人喜欢地上摆一堆脏衣服吧!臭死了!)。

  What goes on the Stack and Heap?

  当代码执行时,堆栈里头主要放置四种

指令:

  在后续文章中再解释,稍安勿躁……

  How is it decided what goes where? (Huh?)

  okok,再啰嗦两句我们就可以正式摆弄我们的堆栈了。

  这里有两条黄金规则:

  引用类型总是保存在堆里头——够清楚了吧

  值类型以及指针,总是保存在其被声明(Declared)的地方。稍微复杂一丁点儿,因为需要理解什么是“其被声明的地方”

  栈,就像我们刚才提到的,负责跟踪单个线程(thread)运行到哪儿了。你不妨把其想象成thread的状态机,每个线程都有自个儿的栈。当我们的代码调某个方法时,线程开始执行JIT编译过并且保存在方法表(method table)中的指令集,同时,它把方法参数压入线程栈中。然后开始执行代码并访问方法里需要的、同时已经存在于线程栈顶部的变量。举个例子吧:

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

  让我们看看栈里头都发生了什么,记住我们看到的只是栈顶的东西,下头早有无数别的东东在里面了哦!

  当我们开始执行这个方法时,方法的参数被压栈(稍后我们讨论参数传递)。

  Notice: 方法并不存在stack里头,图例只是为了演示概念

让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
下一步,控制(线程执行这个方法)被交给AddFive方法在方法表中的指令集,如果这是第一次使用这个方法,JIT编译将被执行。
让你一次性搞定堆、栈、值类型、引用类型-PartA[转]

方法结束了,result被返回。

让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
这时栈顶指针将会移到最初AddFive方法开始的内存地址,这样所有刚才分配的内存空间都被清理掉了,然后接着执行AddFive更下面的函数(图中为显示)。
让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
在这个例子里,result变量被压栈。事实上,任何时候值变量在方法内部被声明,都会被压栈。

  不过,值变量有时候也会存储在堆里头。看看黄金规则二,值类型总是保存在被声明的地方。那么,如果值类型在方法(method)外部声明,同时本身又存在于引用类型内部时,那么就会被保存在堆里头。

 

在来一个例子:

  如果我们有MyInt Class(引用类型):

public class MyInt
{
  public int MyValue;
}

  另外一个方法正在执行中:

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

这个时候开始好玩了:

  因为MyInt是引用类型,因此保存在堆里头,通过栈里的指针去引用它。

让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
AddFive执行完毕以后,我们开始清理栈顶:
让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
这样我们就把MyInt对象当作孤儿留在了堆里头,因为栈中没有任何指针在引用它了!
让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
这时候就是GC大显身手的时候啦!一旦我们调整堆栈中对其引用的地址。可以看到,从性能角度来说,这可真是很费时间。所以现在你是不是觉得注意一下堆栈的处理会有助于你写出高性能的代码呢?

  OK,好了好了,很棒很棒,不过,这东西到底会如何来折磨俺们呢?

  好问题。

  当我们使用引用类型时,我们实际上是使用指向该对象的指针,而不是对象本身。当我们使用值类型时,我们使用的就是值本身。清楚还是不清楚?

最好还是来个例子吧。

  如果我们执行以下方法

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

  我们得到3,够简单吧。

  如果这样呢:

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。

  为什么?

  在第一个例子里所有事都按照预定计划行事:

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

让你一次性搞定堆、栈、值类型、引用类型-PartA[转]


在后面的例子里,我们得不到3是因为x和y都指向堆里的同一个对象。

public int ReturnValue2()
{
   MyInt x;
   x.MyValue = 3;
   MyInt y;
   y = x;        
   y.MyValue = 4;
   return x.MyValue;
}

让你一次性搞定堆、栈、值类型、引用类型-PartA[转]
希望本文能帮助您更好的理解值类型和引用类型基本的区别,以及指针是什么,什么时候会用到指针。在后面的系列中,我们进一步的阐述内存管理,特别的会多聊聊方法参数的问题。

 

相关文章:

  • 2021-10-04
  • 2022-12-23
  • 2022-12-23
  • 2022-02-22
  • 2022-12-23
  • 2022-12-23
  • 2021-08-21
  • 2021-12-25
猜你喜欢
  • 2021-11-16
  • 2021-12-31
  • 2022-01-23
  • 2021-11-19
  • 2021-08-31
  • 2022-03-04
  • 2021-12-26
相关资源
相似解决方案