[编程基础].NET中的c#stack和heap(一)

引用

https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

前言

这段时间在巩固c#基础的时候,发现有些基础性的东西在以前是没有仔细去想过的,比如值类型,和引用类型的区分,究竟值类型保存在哪里,引用类型保存在哪里,两个究竟是啥区别。在说到这个问题的时候,我们需要巩固一下堆和堆栈的信息。这个知识点在前面讲GC的时候有带过一点,但是没有详细去了解过,所以现在开始做一次总结。

Stack 和 Heap 有什么区别

堆栈(stack)一般来负责跟踪代码中正在执行的内容(即所谓的“调用”)。而堆(Heap)一般来说负责跟踪我们的对象。
可以将堆栈想想成一系列盒子堆叠在一起。每次调用方法的时候,都是从顶部叠放一个盒子,从而跟踪应用程序中发生的事情。我们只能使用堆栈顶部盒子里面的内容。当我们完成顶部盒子里的后,我们就把这个盒子给丢掉并继续使用推上来的顶部盒子里的内容。堆是类似的,它的主要用途是保存信息,以便可以随时随地的访问堆中的任何内容。和堆栈相比堆没有任何访问限制
堆栈是自我维护的,这就意味着我们可以不用去管理它的内存情况,顶部的数据不再使用就丢弃掉。堆就必须去考虑垃圾回收的情况(GC)。

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

堆栈和堆上发生了什么

我们将在执行代码时将四种主要类型的东西放入堆栈和堆中:值类型,引用类型,指针和指令。

值类型

在c#中,使用以下类型声明列表表明的所有“事物”均为值类型(因为它们来自System.ValueType):

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

    引用类型

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

  • class
  • interface
  • delegate
  • object
  • string

    指针

    放在内存管理方案“事件”的第三种类型是对类型的引用。引用通常被称为指针。在c#中尽量不要明确使用指针,他们是由公共语言运行时(CLR)管理的。指针(或引用)与引用类型的不同之处在于,当我们说某个对象是引用类型时,意味着我们能够通过指针访问它。指针是内存中的一大块空间,指向内存中的另一个空间。指针占用的空间与我们放入堆栈和堆中的其他任何东西一样。其值可以是内存地址或null。

    指针是可以在堆栈中也可以在堆中的。

    如何决定是放在哪里的

    这是我们的两个黄金法则:

    1. 引用类型总放在堆上。
    2. 值类型和指针总是放在声明它们的地方。
      正如我们前面提到的,堆栈(stack)负责跟踪代码执行过程中每个线程的位置。您可以将其视为线程“状态”,并且每个线程都有自己的堆栈。当我们的代码调用执行一个方法时,线程开始执行经过JIT编译并存放在于方法表中的指令,它还将方法的参数放在线程堆栈中。然后当我们遍历代码并在方法中遇到变量时,将它们放置在堆栈的顶部。

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

      这是堆栈顶部的情况。在查看的内容已经位于堆栈的的其他项目之上了。
      一旦执行executin ghte方法,该方法的参数将被放置在堆栈上。
      注意
      该方法不存在与堆栈中,仅作参考说明

      首先入栈的是方法体,然后入栈我们方法的参数。
      将控制(执行该方法的线程)传递给位于我们类型方法表中的AddFive()方法的指令,如果这是我们第一次点击该方法,将执行JIT编译

      该方法执行时,我们使用一些内存来存储“结果”变量,并将其分配在堆栈上。

      该方法完成执行并返回我们的结果。

      通过将指针移到AddFive()开始的可用内存地址来清理堆栈上分配的所有内存,然后我们转到堆栈上的上一个方法。

      在此实例中,我们的“结果”变量被放置在堆栈上。事实上,每次在方法主体中声明值类型时,它将被放置在堆栈中。
      现在,值类型有时也放置在堆上。在规则中:值类型总是去声明它们的地方。所以,如果在方法外部但是在引用类型内部声明了值类型,那么将其放置在堆的引用类型内。
      现在来看另一个例子:
      如果我们定义以下的MyInt类:

      public class MyInt{
      public int MyValue;
      }

      并且正在执行以下方法:

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

      与之前一样,线程开始执行该方法,并且其参数被放置在该线程的堆栈中。

      现在开始变得有趣了。
      由于MyInt是引用类型,因此将其放置在堆上,并且由堆栈上的指针引用。

      在AddFive()完成执行之后,我们正在清理

      我们在堆中只剩下一个孤零零的MyInt(堆栈中不再有任何人指向MyInt)

      这个时候就是垃圾回收集(GC)发挥作用的地方。一单程序达到一定内存阈值并且需要更多的堆空间时候,便会启动GC。GC将停止所有正在运行的线程(FULL STOP),在堆中找到主程序未访问的所有对象并将其删除。然后,GC将压缩堆中剩余的所有对象以腾出空间,并调整指针指向堆栈和堆中的这些对象。可以想象,这在性能方面可能是非常昂贵的。因此现在您可以了解为什么在尝试编写高性能代码时,注意堆栈和堆中内容是很重要。
      那这是怎么影响我的呢?
      当使用引用类型时,我们要处理类型的指针,而不是该类型本身。当我们使用值类型时,我们使用的是值类型本身。这是不是十分明确呢?
      那我们看下实例代码
      如果我们执行以下方法:

      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。
      可能有人想不通为啥这个值是4
      我们先来先第一个例子中的内存图

      后面我们看下第二个例子的内存图,因为x,y都是指向堆中的同一个对象,所以我们就没办法获得3

      希望以上内容能使您更好的了解c#中“值类型”和“引用类型”变量之间的基本区别,以及对什么是指针以及何时使用它的基本理解。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注