C#中堆与栈的区别

​​​​​​​C# 深入理解堆栈、堆在内存中的实现_遇见--专栏 (1530144568)-CSDN博客_c# 堆栈使用尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(GarbageCollection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们日常程序编写中的变量的行为。在本文中我将讲解栈和堆的基本知识,变量类型以及为什么一些变量能够按照它们自己的方式工作。https://blog.csdn.net/taoerit/article/details/53420684上面这篇博客同样具有较大参考价值,放最前面。

对应的英语原文如下:

C# Heap(ing) Vs Stack(ing) In .NET - Part One

堆与栈都是内存空间的一部分,其中,堆又可以分为托管堆和非托管堆。托管堆和栈由 CLR 管理。

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

对托管堆中的一部分——GC 堆中不用的对象进行释放就是垃圾回收的主要工作,而托管堆的其他部分,和开发者关系相对没有那么大。对非托管堆的管理则需要由开发者完成。

在 CLR 开始执行第一行代码之前,它会先建立三个程序域:系统域、共享域以及默认的一个应用程序域(AppDomain 类型的一个实例)。

其中,开发者无法直接操作系统域和共享域,但 AppDomain 类型的实例可以有多个。

对于简单的程序,例如控制台程序,第一个默认域的名称就是执行文件的全名,例如 abc.exe。可以使用 CreateDomain 方法创建更多的应用程序域。

每一个 AppDomain 的实例都有自己的加载堆,下面就会介绍加载堆到底是什么。

这里的堆(heap)是托管堆(managed heap)的简称。顾名思义,它由 CLR 进行管理。

它是在运行程序时,CLR 申请的一块内存空间。它基于进程,属于进程内存空间的一部分。

这块空间可以划分为下面几个主要部分:

  • (至少)三个程序域,以及它们自带的加载堆和其他零部件。
  • GC 堆(GC heap):垃圾收集器的处理对象。它分为 0, 1, 2 代三块区域,越高代的堆大小越大。


大对象堆(large object heap)是 2 代堆的一部分,它存放超过 85KB 大小的对象。

因为大对象堆里面的对象太大,移动代价过高,所以微软设计的意图是直接将它提升到 2 代,避免升代移动引起的性能损失。

其中,加载堆(loader heap)存在于每一个程序域中,存放 CLR 自己的类型系统以及用户定义的类型对象。

不同域的加载堆存放的对象不同。另外,AppDomain 的加载堆也存放静态对象,由于静态对象是全局的,不会成为垃圾,所以加载堆不受垃圾收集器管辖。

加载堆又可以分为高频堆(大小为 32KB),低频堆(大小为 8KB)等。顾名思义,高频堆存放的是 CLR 认为访问次数可能较多的对象,例如类型方法表等。

下图简单展示了托管堆的结构。
 

托管堆结构简图


还有其他的堆,例如 JIT 代码堆,用来存放 JIT 之后的本地代码,但一些较不常见的堆并不重要。

这里记住垃圾收集器只会光顾 GC 堆就可以了。

当创建新对象时,若该对象是引用类型或者包括引用类型的值类型,就会在 GC 堆上申请空闲的内存空间,CLR 会先计算需要的空间大小。

如果堆上已经没有剩余空间了,就触发一次垃圾回收。

如果回收之后仍然无法获得足够的剩余空间,则掷出 OutOfMemory 异常。

GC 堆维护一个叫做 NextObjPtr 的指针,指向 GC 堆的下一个可用地址。

为了尽量合理利用空间,GC 堆的内存分配是连续的。

当垃圾收集结束之后,由于有些空间被释放,内存可能出现碎片,此时,会进行压缩,将内存重新变回连续状态。

当然,GC 堆只是全部内存资源的一小部分。

对于非托管资源来说,它们会占用另一部分的内存。 这块空间叫做本地堆(Native Heap),或者非托管堆(Unmanaged Heap),CLR 不负责这块 空间的垃圾回收。

非托管资源有很多,比如文件流、数据库连接、打印机资源等。

如果没有妥善地处理非托管资源,就会发生各种稀奇古怪的错误。

栈(stack),又称“线程栈”(thread stack),顾名思义,它是基于线程的。

它的空间比较小,在每开启一个新的线程时,从内存中开辟大约 1M 空间,作为该线程的自留地。

线程栈是一个先进后出的栈数据结构,所以它一直都是连续的。

CLR 维护一个指针,指向栈的下一个自由空间的地址,当成员出入栈时,指针地址跟着发生变化。

因为栈中的对象离开了定义域就会被自动销毁,通常栈的空间是够用的。

不过,如果程序写的有问题,还是可能会爆栈,此时就会掷出大名鼎鼎的 StackOverflow 异常。

写岀一个爆栈的程序很容易,例如一个没有出口的递归即可,此时,所有的变量都还在它们的定义域中。

对于非静态的、纯粹的值类型(例如,不包含任何引用类型成员的结构体),初始化时,CLR 会计算它需要的空间大小,然后将其值存储在栈上。例如,一个 int 的大小是 4 个字节。

而对于引用类型,它也会使用栈,但栈上仅仅存储一个地址(即引用),就是它在托管堆上的内存地址。

通过访问栈上的地址,就可以间接访问到堆上的引用类型对象,以及对象真正的成员和它们的值。

由于栈有着得天独厚的优势(只能从顶部放入和拿走数据),栈中的内存总是连续的,不需要进行 GC。

参考:

C#/.NET堆与栈

首先堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每个进程的虚拟内存为4GB)

堆栈stack

堆栈中存储值类型。

堆栈实际上是向下填充,即由高内存地址指向地内存地址填充。

堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。

堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!

堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。

通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆(托管堆)!

堆(托管堆)heap

堆(托管堆)存储引用类型。

此堆非彼堆,.NET中的堆由垃圾收集器自动管理。

与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。

比如创建一个对象:

Customer cus;

cus = new Customer();

申明一个Customer的引用cus,在堆栈上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的Customer对象!

cus占4个字节的空间,包含了存储Customer的引用地址。

接着分配堆上的内存以存储Customer对象的实例,假定Customer对象的实例是32字节,为了在堆上找到一个存储Customer对象的存储位置。

.NET运行库在堆中搜索第一个从未使用的,32字节的连续块存储Customer对象的实例!

然后把分配给Customer对象实例的地址赋给cus变量!

从这个例子中可以看出,建立对象引用的过程比建立值变量的过程复杂,且不能避免性能的降低!

实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!

有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。

当一个应用变量出作用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。

线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap

栈与堆的区别
栈通常保存着代码执行的步骤,如在代码段1中 AddFive()方法,int pValue变量,int result变量等等。而堆上存放的则多是对象,数据等。可以把栈想象成一个接着一个叠放在一起的盒子。当使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走,接着下一个。堆则不然,像是一个仓库,储存着使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
栈内存无需管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。

参考:C#堆和堆栈有什么区别_百度知道

这里有一条黄金规则:

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

2. 值类型和指针总是放在它们被声明的地方。

(这条稍微复杂点,需要知道栈是如何工作的,然后才能断定是在哪儿被声明的。)

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

下面是一个方法(Method):

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

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

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

堆和栈:程序运行时的内存区域

     我们把内存分为堆空间和栈空间。

      栈空间比较小,但是读取速度快

      堆空间比较大,但是读取速度慢

1.栈

     栈的特征:

     数据只能从栈的顶端插入和删除

     把数据放入栈顶称为入栈(push)

     从栈顶删除数据称为出栈(pop)

     简单地说:先进后出

2.堆

    堆是一块内存区域,与栈不同,堆里的内存能够以任意顺序存入和移除

        

3.值类型和引用类型 

  类型被分为两种:值类型(整数,bool struct char 小数)和引用类型(string 数组 自定义的类,内置的类)。

  1)值类型只需要一段单独的内存,用于存储实际的数据,(单独定义的时候放在栈中)

  2)引用类型需要两段内存

  第一段存储实际的数据,它总是位于堆中

  第二段是一个引用,指向数据在堆中的存放位置

      

注意: 但我们使用引用类型赋值时,其实是赋值的引用类型的引用,如果数组是一个值类型的数组,那么数组中直接存储值,如果是一个引用类型的数组(数组中存储的是引用类型),那么数组中存储的是引用(内存地址)。

参考:c#中堆和栈的区别_weixin_41925938的博客-CSDN博客_c#堆和栈的区别

C#中栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;堆是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小

线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap

使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。

参考:浅谈C#中堆和栈的区别 - 哈哈哈呵呵呵 - 博客园

猜你喜欢

转载自blog.csdn.net/u012842630/article/details/121665708
今日推荐