堆 Heap & 栈 Stack(.Net)【概念解析系列_3】【C# 基础】

heap,stack,net,概念,解析,系列,c#,基础 · 浏览次数 : 349

小编点评

**堆栈** 堆栈是一种基于后进先出(LIFO)原则的内存区域,用于存储程序运行时需要的数据。当程序调用一个方法时,该方法的参数、返回地址和局部变量等数据会被压入栈中。当方法执行结束时,这些数据会从栈中弹出。 **托管堆** 托管堆是.NET中用于存储托管对象的内存区域,由 CLR(Common Language Runtime)负责管理。所有的托管对象都分配在托管堆上,并由 CLR 自动进行内存管理。当需要创建一个对象时,CLR 会在托管堆上为其分配内存;而当对象不再被引用时,垃圾回收机制会定期清理无用的对象,并回收它们所占用的内存空间。 **非托管堆** 非托管堆是指 .NET 应用程序使用的非托管资源的内存区域,例如通过使用平台调用接口(P/Invoke)调用的外部库或操作系统资源。非托管堆通常由操作系统或外部库分配和管理,而 .NET 应用程序通过与非托管代码进行交互来访问和操作非托管堆。 **垃圾回收机制** 垃圾回收器是 CLR 中的组件,负责管理托管堆中的内存。它会周期性地扫描托管堆,标记并清理不再被引用的对象,并释放其占用的内存空间。垃圾回收机制通过追踪对象之间的引用关系,确定哪些对象可以被安全地回收,以避免内存泄漏和资源浪费。

正文

〇、前言

本文主要围绕 .Net 框架中的托管堆(Heap,简称堆)和堆栈(Stack,简称栈)展开。

.Net 程序在 CLR(Common Language Runtime 公共语言运行时)上运行时,内存被从逻辑上划分为两个主要部分:堆和栈。除了栈和堆之外,CLR 还维护了其他一些内存区域,例如静态存储区域(Static Storage Area)、常量存储区域(Constant Storage Area)等。这些内存区域都有各自的特点和用途,可以帮助我们更好地管理程序内存和资源的使用。

因此,熟知堆和栈的运行机制,对提升系统性能和稳定性至关重要。

一、值类型和引用类型

在介绍主角之前我们先来了解一下值类型和引用类型。

1.1 值类型

先看下都有哪些类型属于值类型:

类别 例举
整型数值类型
sbyte(-128~127)、byte(0~255)、
short(-32768~32767)、ushort(0~65535)、
int(-2147483648~2147483647)、uint(0~2147483647)、
long(-9,233,372,036,854,775,808~9,223,372,036,854,775,807)、ulong(0~18,446,744,073,709,551,615)、
nint(取决于在运行时计算的平台:带符号的 32 位或 64 位整数)、
nuint(取决于在运行时计算的平台:无符号的 32 位或 64 位整数)
浮点型数值类型
float(f/F)(2.77f、2.77F、1_000.000_012f、1_000.000_012F)
double(d/D)(3d、3D、3.000_012) // 可以不用带字母
decimal(m/M)(3_000.5m、400.75M)
布尔类型
bool check = true; // true / false
Unicode UTF-16 字符
char(U+0000~U+FFFF)
(三种方法指定 char 值:
    1. 字符文本;
    2. Unicode 转义序列,它是\u后跟四位字符代码的十六进制表示形式;
    3. 十六进制转义序列,它是 \x 后跟字符代码的十六进制表示形式)
枚举类型
// 默认情况下,枚举成员的关联常数值为类型 int;它们从零开始,并按定义文本顺序递增 1。
// 也可以显式指定任何其他整数数值类型作为枚举类型的基础类型。
// 还可以显式指定关联的常数值,如下示例:
enum ErrorCode : ushort
{
    None = 0,
    Unknown = 1,
    ConnectionLost = 100,
    OutlierReading = 200
}
结构类型(structure/struct type)
// 结构类型具有值语义。也就是说,结构类型的变量包含类型的实例。
// 默认情况下,在分配中,通过将参数传递给方法并返回方法结果来复制变量值。
// 对于结构类型变量,将复制该类型的实例。
public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }
    public double X { get; }
    public double Y { get; }
    public override string ToString() => $"({X}, {Y})";
}
// 可以使用结构类型来设计以数据为中心的较小类型,这些类型只有很少的行为或没有行为。
// 例如,.NET 使用结构类型来表示数字(整数和实数)、布尔值、Unicode 字符以及时间实例。
元组类型
// 元组功能提供了简洁的语法来将多个数据元素分组成一个轻型数据结构。
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

// 如前面的示例所示,若要定义元组类型,需要指定其所有数据成员的类型,或者,可以指定字段名称。
// 虽然不能在元组类型中定义方法,但可以使用 .NET 提供的方法,如下面的示例所示
(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

1.2 引用类型

再来看下哪些属于引用类型:

类别 例举
内置引用类型
object 对象类型。可以将任何类型的值赋给 object 类型的变量
string 字符串类型。string 类型表示零个或多个 Unicode 字符的序列。定义 == 和 != 是为了比较 string 对象(而不是引用)的值
    string a = "hello";
    string b = "h";
    // Append to contents of 'b'
    b += "ello";
    Console.WriteLine(a == b); // true
    Console.WriteLine(object.ReferenceEquals(a, b)); // false
string 字符串文本。三种类型:原始、带引号、逐字。
    原始字符串,就是至少三个双引号 (""") 括起来,"且自动忽略最左侧的空格:""" This is a text.""";(C# 11 可用)
    带引号字符串,就是常用的双引号方式:"This is a text.";
    逐字字符串,就是在双引号前加符号 @:@"This is a text."
UTF-8 字符串。 从 C# 11 开始,可以将 u8 后缀添加到字符串字面量以指定 UTF-8 编码。 UTF-8 字面量存储为 ReadOnlySpan<byte> 对象
    ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
    ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8;
    byte[] AuthStringLiteral = "AUTH "u8.ToArray();
委托类型。它有一个返回值和任意数目任意类型的参数,如下示例:
    public delegate void MessageDelegate(string message);
    public delegate int AnotherDelegate(MyType m, long num);
    Action<string> stringAction = str => {};
    Action<object> objectAction = obj => {};
    // Creates a new delegate instance with a runtime type of Action<string>.
    Action<string> wrappedObjectAction = new Action<string>(objectAction);
    // The two Action<string> delegate instances can now be combined.
    Action<string> combination = stringAction + wrappedObjectAction;
动态类型。dynamic 类型表示变量的使用和对其成员的引用绕过编译时类型检查,改为在运行时解析这些操作。
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;
        System.Console.WriteLine(dyn.GetType());
        // dyn = dyn + 3;
        // obj = obj + 3; // 编译错误
        System.Console.WriteLine(obj.GetType());
    }
    // 对于 dyn + 3,不会报告任何错误。 在编译时不会检查包含 dyn 的表达式,原因是 dyn 的类型为 dynamic
记录
// 从 C# 9 开始,可以使用 record 修饰符定义一个引用类型,用来提供用于封装数据的内置功能
// C# 10 允许 record class 语法作为同义词来阐明引用类型,并允许 record struct 使用相同功能定义值类型
public record Person(string FirstName, string LastName);
public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}
// 使用 class 关键字声明类
class TestClass
{
    // Methods, properties, fields, events, delegates
    // and nested classes go here.
}
接口
// 接口定义协定。实现该协定的任何 class 或 struct 必须提供接口中定义的成员的实现
interface ISampleInterface // 定义接口
{
    void SampleMethod();
}
class ImplementationClass : ISampleInterface // 实现接口
{
    void ISampleInterface.SampleMethod()
    {
        // 执行的操作详情
    }
    static void Main()
    {
        ISampleInterface obj = new ImplementationClass(); // 声明一个接口的实例
        obj.SampleMethod(); // 调用接口方法
    }
}

1.3 值类型与引用类型的对比

  • 值类型变量声明后,不管是否已经赋值,编译器为其分配内存。
  • 引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
  •  
  • 值类型的实例通常是在线程栈上分配的(静态分配),但是在某些情形下可以存储在堆中。
  • 引用类型的对象总是在进程堆中分配(动态分配)。
  •  
  • 值类型的变量直接包含类型的实际值。
  • 引用类型包含对类型实例的引用。
  •  
  • 两个不同的变量不能指向同一个值类型的值。
  • 两个不同的变量可以指向同一个引用类型的值,只是这两个变量存的内存地址引用相同。

参考:https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/value-types

二、关于堆和栈的理解

2.1 堆和栈的概念

堆(托管堆和非托管堆)

  托管堆:

  其实在 C 语言中才叫堆,C# 中叫托管堆。托管堆是由 CLR(公共语言运行库 Common Language Runtime)管理,当堆中满了之后,会自动清理堆中的垃圾,因此加了“托管”两字。这也是为什么 .Net 开发不需要关心内存释放的原因。

  托管堆是一块动态分配的内存区域,用于存储程序运行时需要的数据。当声明一个引用类型对象或变量时,它们被分配到堆上,并返回其引用(即指向该对象或变量在堆中存储位置的指针)。堆中的对象或变量可以通过其引用来访问和修改。  

  非托管堆:

  .NET的程序还包含了非托管的堆,所有需要分配堆内存的非托管资源将会被分配到非托管堆上。非托管的堆需要程序员用指针手动地分配,并且手动地释放,.NET的垃圾回收和内存管理制度不适用于非托管的堆。

  常见的非托管资源有:文件流、图像图形类、数据库的连接,网络连接,系统的窗口句柄,打印机资源等。

  

  栈是一种基于后进先出(Last In First Out,LIFO)原则的内存区域。栈存储几种类型的数据:某些类型变量的值、程序当前的执行环境、传递给方法的参数。当程序调用一个方法时,该方法的参数、返回地址和局部变量等数据会被压入栈中。当方法执行结束时,这些数据会从栈中弹出。

  本质上讲堆栈也是一种线性结构,符合线性结构的基本特点:即每个节点有且只有一个前驱节点和一个后续节点。

  栈把所有操作限制在“只能在线性结构的某一端”进行,而不能在中间插入或删除元素。把数据放入栈顶称为入栈(push), 从栈顶删除数据称为出栈(pop)。

  

2.2 堆和栈的关系

内存空间分配

  • 堆:开发人员可以根据实际需要进行动态扩展堆的大小,但过多的使用堆,会影响程序性能和稳定性。
  • 栈:栈的大小一般比较有限,不能在栈上存储过多的数据,一般用于存放函数的参数值、局部值类型变量的值、引用类型的引用等。

缓存方式

  • 堆:存放在二级缓存中,生命周期由垃圾回收算法来决定,并非弃用后立马被回收,所以调用这些对象的速度要相对来得低一些。
  • 栈:使用的是一级缓存, 他们通常都是被调用时从栈顶取出,用完即释放。

数据结构

  • 堆:堆中一般保存的是实际的数据值,例如引用类型的实例值。
  • 栈:栈中一般存储较小体量的值,例如值类型的值、引用类型的引用等。

存取排列方式

  • 堆:堆里的内存能够以任意顺序存入和移除。
  • 栈:LIFO 后进先出。必须从栈顶进行存取操作。

三、堆栈是如何配合来保证程序的正常运行的?

在.NET中,堆栈(stack)、托管堆(managed heap)、非托管堆(unmanaged heap)和垃圾回收机制配合使用来保证程序的正常运行。以下是它们之间的协同工作方式:

  • 堆栈:堆栈用于管理函数调用和局部变量,确保函数的正确执行和返回。每个线程都有一个私有的堆栈,用于存储函数调用的上下文信息。当一个函数被调用时,相关的信息会被压入堆栈;而当函数执行结束后,这些信息会从堆栈中弹出。堆栈的快速分配和释放特性使得函数调用能够高效地进行。
  • 托管堆:托管堆是.NET中用于存储托管对象的内存区域,由 CLR(Common Language Runtime)负责管理。所有的托管对象都分配在托管堆上,并由 CLR 自动进行内存管理。当需要创建一个对象时,CLR 会在托管堆上为其分配内存;而当对象不再被引用时,垃圾回收机制会定期清理无用的对象,并回收它们所占用的内存空间,防止内存泄漏和资源浪费。
  • 非托管堆:非托管堆是指 .NET 应用程序使用的非托管资源的内存区域,例如通过使用平台调用接口(P/Invoke)调用的外部库或操作系统资源。非托管堆通常由操作系统或外部库分配和管理,而 .NET 应用程序通过与非托管代码进行交互来访问和操作非托管堆。
  • 垃圾回收机制:垃圾回收器是 CLR 中的组件,负责管理托管堆中的内存。它会周期性地扫描托管堆,标记并清理不再被引用的对象,并释放其占用的内存空间。垃圾回收机制通过追踪对象之间的引用关系,确定哪些对象可以被安全地回收,以避免内存泄漏和资源浪费。

堆栈管理函数调用和局部变量,托管堆管理托管对象的分配和回收,非托管堆用于存储和管理非托管资源,而垃圾回收机制定期清理无用的对象,释放内存资源。这种机制确保了程序的内存使用效率和稳定性,减轻了开发人员对内存管理的负担,使他们能够更专注于应用程序的逻辑开发。

参考:https://blog.csdn.net/qq_44034384/article/details/106611384  https://qianwen.aliyun.com/chat  https://blog.csdn.net/beenles/article/details/130710732

与堆 Heap & 栈 Stack(.Net)【概念解析系列_3】【C# 基础】相似的内容:

堆 Heap & 栈 Stack(.Net)【概念解析系列_3】【C# 基础】

在.NET中,堆栈(stack)、托管堆(managed heap)、非托管堆(unmanaged heap)和垃圾回收机制配合使用来保证程序的正常运行。

7.1 C/C++ 实现动态数组

动态数组相比于静态数组具有更大的灵活性,因为其大小可以在运行时根据程序的需要动态地进行分配和调整,而不需要在编译时就确定数组的大小。这使得动态数组非常适合于需要动态添加或删除元素的情况,因为它们可以在不浪费空间的情况下根据需要动态增加或减少存储空间。动态数组的内存空间是从堆(heap)上分配的,动态数组需要程序员手动管理内存,因为它们的内存空间是在程序运行时动态分配的。程序员需要在使用完动态数组后

平衡树 Treap & Splay [学习笔记]

平衡树 \(\tt{Treap}\) & \(\tt{Splay}\) 壹.单旋 \(\tt{Treap}\) 首先了解 \(\tt{BST}\) 非常好用的东西,但是数据可以把它卡成一条链 \(\dots\) 于是,我们将 \(\tt{Tree}\) 与 \(\tt{heap}\) (堆) 合并,

[转帖]查看堆内对象的工具:jmap

文章目录 用途命令格式示例一,no option二,heap三,histo[:live]四,clstats五,finalizerinfo六,dump: 用途 jmap 可以查看堆内对象的信息,生成 java 程序的 dump 文件,甚至可以查看 ClassLoader 的

[转帖]关于Java:是否可以覆盖-XX + HeapDumpOnOutOfMemoryError生成的堆转储的文件权限?

https://www.codenong.com/12484559/ Can you override the file permissions for the heap dump produced by -XX+HeapDumpOnOutOfMemoryError? 在Linux上,使用-XX+H

[转帖]【JVM】堆内存与栈内存详解

堆和栈的定义 java把内存分成栈内存和堆内存。 (1)栈内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。 当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另

“堆内存持续占用高 且 ygc回收效果不佳” 排查处理实践

内存占用持续居高不下,频繁young gc且效果不佳,究竟出现了什么问题?young gc的时机? 为何young gc后堆内存使用率仍然很高?又是什么原因导致内存占用高?本篇文章将深度解析其原因并提供一套为止可行的解决方案。

堆的原理以及实现O(lgn)

大家好,我是蓝胖子,我一直相信编程是一门实践性的技术,其中算法也不例外,初学者可能往往对它可望而不可及,觉得很难,学了又忘,忘其实是由于没有真正搞懂算法的应用场景,所以我准备出一个系列,囊括我们在日常开发中常用的算法,并结合实际的应用场景,真正的感受算法的魅力。 今天我们就来看看堆这种数据结构。 源

常回家看看之堆溢出

对于堆溢出,有很多漏洞可以和它打配合,可以说是堆里面很常见的漏洞,常见的有off_by_null,House系列(后续学习到了会继续更新这个系列),unlink,等等。 今天来看一个,堆溢出修改指针,导致libc泄露以及通过指针来修改got表的题目。 题目连接我放下面了,对堆有兴趣的可以去看看✔

JVM 堆外内存查看方法

JVM 堆外内存查看方法JVM 堆外内存查看方法 1.概述 是否曾经想过为什么Java应用程序通过众所周知的*-Xms和-Xmx调整标志消耗的内存比指定的数量大得多 ?由于各种原因和可能的优化,JVM可能会分配额外的本机内存。这些额外的分配最终可能使消耗的内存超出-Xmx* 限制。 在本教程中,我们