优秀的编程知识分享平台

网站首页 > 技术文章 正文

C#中堆栈(Stack)和堆(Heap)的区别——第一部分

nanyue 2024-11-12 11:47:39 技术文章 2 ℃

即使在.NET框架,我们也不必主动担心内存管理和垃圾回收(GC),但仍必须牢记内存管理和垃圾回收,以优化应用程序的性能。另外,对内存管理的工作原理有基本的了解将有助于解释我们在编写的每个程序中使用的变量的行为。在本文中,我将介绍堆栈(Stack)和堆(Heap)的基础知识,变量的类型以及为什么某些变量会如此工作。

当您的代码执行时,.NET框架有两个位置将项目存储在内存中。如果您还没有见过面,请允许我向您介绍堆栈(Stack)和堆(Heap)。堆栈(Stack)和堆(Heap)都可以帮助我们运行代码。它们驻留在我们机器上的操作内存中,并包含我们需要的所有信息。


堆栈(Stack) vs. 堆(Heap):有什么区别?

堆栈(Stack)或多或少地负责跟踪代码中正在执行的内容(或所谓的“调用”)。堆(Heap)或多或少负责跟踪我们的对象(我们的数据,好吧...大部分-我们稍后再讨论)。

可以将“堆栈(Stack)”视为一系列堆叠在一起的箱子。每次调用方法(称为框架)时,我们都要将另一个方框堆叠在上面,以便跟踪应用程序中发生的事情。我们只能使用堆栈(Stack)顶部框中的内容。当我们完成了顶部框(方法已完成执行)后,我们将其丢弃并继续使用堆栈顶部的上一个框中的内容。堆(Heap)是类似的,除了它的用途是保存信息(大部分时间不跟踪执行情况),以便可以随时访问堆(Heap)中的任何内容。使用堆(Heap),就可以像在堆栈(Stack)中那样没有任何限制的访问任何内容。堆(Heap)就像我们床上的干净衣物堆一样,我们还没有花时间将它们收起来-我们可以迅速拿起我们需要的东西。堆栈(Stack)就像壁橱里的鞋盒一样,我们必须把最上面的那个拿下来,才能拿到最下面的那个。

上面的图片虽然不能真正表示内存中发生的事情,但可以帮助我们将堆栈(Stack)与堆(Heap)区分开。

堆栈(Stack)是自我维护的,这意味着它基本上负责自己的内存管理。如果不再使用顶部框,则将其丢弃。另一方面,堆(Heap)必须使用垃圾收集(GC),以便保持堆(Heap)干净。


堆栈和堆上发生了什么?

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


值类型

在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)管理的。指针(或引用)与引用类型的不同之处在于,当我们说某种东西是引用类型时,意味着我们可以通过指针访问它。指针是内存中的一大块空间,指向内存中的另一个空间。就像我们放入堆栈(Stack)和堆(Heap)中的其他任何东西一样,指针占用空间,其值可以是内存地址或null。


指令

您将在本文后面看到“指令”的工作方式...


如何决定去哪里?(Huh?)

好的,最后一件事,我们将介绍有趣的东西。

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

  • 引用类型总是放在堆(Heap)上-很简单,对吧?
  • 值类型和指针总是放在声明它们的地方。这稍微复杂一点,并且需要了解堆栈(Stack)如何工作,便于理解“事物”被声明的位置。

正如我们前面提到的,堆栈(Stack)负责跟踪代码执行过程中每个线程的位置(或所谓的)。您可以将其视为线程“状态”,并且每个线程都有自己的堆栈(Stack)。当我们的代码调用执行一个方法时,线程开始执行已经被JIT编译并存在于方法表中的指令,它还将方法的参数放在线程堆栈(Stack)中。然后,当我们遍历代码并在方法中遇到变量时,将它们放置在堆栈的顶部。通过示例最容易理解...

采取以下方法。


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

这是堆栈(Stack)顶部的情况。请记住,我们正在查看的内容位于已存在于堆栈(Stack)中的许多其他项之上:

一旦开始执行executin ghte方法,该方法的参数就会放在堆栈(Stack)上(稍后我们将详细讨论传递参数)。


注意

该方法不存在于堆栈中,仅作参考说明。

接下来,将控制(执行该方法的线程)传递给位于我们类型的方法表中的AddFive()方法的指令,如果这是我们第一次点击该方法,则将执行JIT编译。

该方法执行时,我们需要一些用于“结果”变量的内存,并将其分配在堆栈(Stack)上。

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

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

在此示例中,我们的“结果”变量被放置在堆栈(Stack)上。事实上,每次在方法主体中声明值类型时,它将被放置在堆栈(Stack)中。

现在,值类型有时也放置在堆上。还记得规则,值类型总是去声明它们的地方吗?好吧,如果在方法外部但在引用类型内部声明了值类型,则将其放置在堆的引用类型内。

这是另一个例子。

如果我们具有以下MyInt类(因为它是一个类,所以是引用类型):


Public MyInt   
{            
   public int  MyValue;   
} 

并且正在执行以下方法:


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

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

现在是时候变得有趣了...

由于MyInt是引用类型,因此将其放置在堆(Heap)上并由堆栈(Stack)上的指针引用。

在AddFive()完成执行之后(如第一个示例一样),我们正在清理...

我们在堆(Heap)中只剩下一个孤立的MyInt(堆栈中不再有任何类指向MyInt)!

这就是垃圾收集(GC)发挥作用的地方。一旦我们的程序达到一定的内存阈值并且我们需要更多的堆(Heap)空间,我们的GC将启动。GC将停止所有正在运行的线程(FULL STOP),在堆中找到主程序未访问的所有对象并将其删除。然后,GC将重新组织堆中剩余的所有对象以腾出空间,并调整所有指针指向堆栈(Stack)和堆(Heap)中的这些对象。可以想象,这在性能方面可能是非常昂贵的,因此现在您可以了解为什么在尝试编写高性能代码时,注意堆栈(Stack)和堆(Heap)中的内容很重要。

好的,那太好了,但这对我有何影响?

**好问题。 **

当我们使用引用类型时,我们正在处理类型的指针,而不是事物本身。当我们使用值类型时,我们使用的是事物本身。像泥一样清澈,对不对?

再次,这是最佳示例。

如果我们执行以下方法:


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

在下一个示例中,我们没有得到“ 3”,因为变量“ x”和“ y”都指向堆中的同一对象。


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

希望这可以使您更好地理解C#中“值类型”和“引用类型”变量之间的基本区别,以及对什么是指针以及何时使用它的基本理解。在本系列的下一部分中,我们将进一步研究内存管理,并专门讨论方法参数。

Tags:

最近发表
标签列表