文章目录

  • 前言
  • 一、栈与堆
    • 1.1 栈(Stack)
      • 1.1.1 基本信息
      • 1.1.2 特点
    • 1.2 堆(Heap)
      • 1.2.1 基本信息
      • 1.2.2 特点
    • 1.3 从代码中窥见堆栈
  • 二、装箱与拆箱
    • 2.1 装箱
    • 2.2 拆箱
    • 2.3 如何避免不必要的装箱与拆箱
      • 2.3.1 泛型集合
      • 2.3.2 泛型参数
  • 总结


前言

编写一个健壮的程序离不开对资源的高效利用,这里说的无非就是内存,算力。我们基于.NET平台编写程序的时候,了解内存机制,对程序的性能与运行稳定性都会有帮助。

本篇文章将介绍堆(Heap)和栈(Stack)这两种基础内存区域,了解程序运行的时候堆和栈是如何决定数据的存储与访问方式。并且探究装箱与拆箱是如何偷走我们程序的内存和无端消耗资源的,以及如何去避免。下面就开始深入理解堆与栈。


一、栈与堆

程序运行时,CLR 在操作系统提供的虚拟地址空间基础上的,将虚拟内存空间划分和管理为多个区域,其中堆和栈是 C# 中最核心的两个数据存储区域(也可以称之为托管堆,托管栈)。这两者采用不同的数据结构,存储的内容也不同,性能差异上也有巨大的差异。下面分别就二者的设计目的分别介绍。
CLR

CLR是.NET框架的核心组件,负责C#这类托管代码的执行。运行在NET平台上的程序,其内存管理正是由CLR调度,CLR在操作系统提供的虚拟地址空间基础上划分托管内存区域,这正包括了堆和栈。

值类型和引用类型

值类型和引用类型是 C# 类型的两个主要类别。 值类型的变量包含类型的实例。 它不同于引用类型的变量,后者包含对类型实例的引用。 默认情况下,在分配中,通过将实参传递给方法并返回方法结果来复制变量值。
对于值类型,每个变量都有其自己的数据副本,并且一个变量上的作不会影响另一个变量。
对于引用类型,两种变量可引用同一对象。因此,对一个变量执行的操作会影响另一个变量所引用的对象。

1.1 栈(Stack)

1.1.1 基本信息

栈是一种先进后出(LIFO)的连续内存区域,由CLR自动管理分配,它存储的是值类型、引用类型的引用和方法上下文。

  1. 值类型无非就整型数值(sbyte,byte,short,ushort,int,uint,long,ulong,nint,nuint)浮点类型(float,double,decimal)、布尔型(bool),字符型(char),枚举类型(enum)和结构类型(struct)。
  2. 引用类型的实际数据存储在堆中,但其 "指针"存储在栈上,也就是引用类型的引用。
  3. 方法的上下文内容包括方法参数、局部变量、和返回地址等。其中局部变量包括值类型和引用类型的引用

当程序调用一个方法的时候,CLR会在栈上创建一个栈帧(Stack Frame)。这个栈帧用于存储方法的参数;方法内的局部变量,如果是值类型就存它本身,引用类型存储其引用类型的引用;方法执行完后回到调用处的位置的返回地址。

1.1.2 特点

栈的内存分配是连续的,由CLR自动管理。由于它是一片连续的内存,无需复杂操作就能实现入栈 和出栈,分配和释放速度极快。当一段方法执行完毕,也就是数据超出了作用域范围,其栈上的内存会被自动释放。但是栈的内存空间很小,几MB的大小,不适合存储大批量数据。

回想在基于C语言的开发中,经常是手动申请栈空间和手动释放栈内存,稍有不慎就会造成栈溢出。

1.2 堆(Heap)

1.2.1 基本信息

比起小且连续的栈。堆是一种无序结构的大内存区域。.NET的GC(垃圾回收器)自动管理内存的分配和释放。堆主要用来存储引用类型本身。

引用类型大致可以分成两类,一类是需要用显式声明引用类型(class,interface,delegate,record),还有一类是.NET内置的基础引用类型(dynamic类型,object,string)

1.2.2 特点

前面提到堆是无序结构的大内存区域,在堆上面内存分配需要查找可用空间。对堆内存的释放也依赖 GC的定期清理,这里面是有一部分的性能开销存在的。虽然开发者无需手动释放堆内存,GC 会自动回收不再被引用的对象。但是频繁分配和释放可能导致不连续的空闲空间,GC虽然也会自动进行压缩操作会缓解但也有开销的存在。这种不连续的空闲空间进一步减慢了分配速度。

1.3 从代码中窥见堆栈

分别声明一个结构体(值类型)和一个类(引用类型),结构体是存储在栈上,类的实例存储在堆上,变量仅保存引用地址存放在栈上。

值类型之间的复制传递的是栈上的值,也就是复制一个新的结构体的时候,是在栈上开辟一个新的空间保存原始结构体的值。

引用类型之间复制虽然本质上传递的也是栈上的引用,复制一个新的类的时候,也会在栈上开辟一个空间存储类型的引用。这个引用地址指向堆,也就是类实例实际存放数据的位置。

这种特性就引申出了一个经典的话题,深拷贝和浅拷贝。
对于值类型来说,原始值类型和被复制的值类型之间数据是相互独立的,它们保存在栈上的不同空间。对其中一个的修改,不会影响到对方。
对于引用类型,赋值时复制的是引用。原始对象和复制对象之间的在栈上虽然不是保存在一个位置,但是保存的都是同一个引用。也就是说如果通过其中一个栈上引用找到的堆上数据进行修改,也会影响到另一个对象。

Console.WriteLine("================== 结构体(栈存储)===========================");
StackItem item1 = new StackItem(1, "原始结构体");
StackItem item2 = item1;  //复制整个值到栈上的新位置
Console.WriteLine($"修改前 - item1: Id={item1.Id}, Data={item1.Data}");
Console.WriteLine($"修改前 - item2: Id={item2.Id}, Data={item2.Data}");
item2.Id = 2;
item2.Data = "修改后结构体";  //只修改栈上的副本
Console.WriteLine($"修改后 - item1: Id={item1.Id}, Data={item1.Data}");  //原始值不变
Console.WriteLine($"修改后 - item2: Id={item2.Id}, Data={item2.Data}");  //副本被修改Console.WriteLine("================== 类(堆存储)===========================");
HeapItem obj1 = new HeapItem(1, "原始对象");  // 对象在堆上,obj1是栈上的引用
HeapItem obj2 = obj1;  // 复制引用(栈上的地址),指向同一个堆对象Console.WriteLine($"修改前 - obj1: Id={obj1.Id}, Data={obj1.Data}");
Console.WriteLine($"修改前 - obj2: Id={obj2.Id}, Data={obj2.Data}");obj2.Id = 2;
obj2.Data = "修改后对象";  //通过引用修改堆上的同一个对象Console.WriteLine($"修改后 - obj1: Id={obj1.Id}, Data={obj1.Data}");  // 原始对象被修改
Console.WriteLine($"修改后 - obj2: Id={obj2.Id}, Data={obj2.Data}");  // 引用指向的对象被修改public struct StackItem {public int Id;public string Data; public StackItem(int id, string data){Id = id;Data = data;}
}public class HeapItem
{public int Id;public string Data;public HeapItem(int id, string data){Id = id;Data = data;}
}

二、装箱与拆箱

值类型和引用类型是之间是能相互转换的,比如object是所有类型的最终基类,自然也是值类型的基类。特定条件下值类型能转换成object,object也能转换为值类型。前者值类型转换成引用类型称之为装箱,后者引用类型转换为值类型称之为拆箱。

值类型与引用类型之间转换的两种操作背后是内存里栈和堆的转换。这里面涉及内存分配、数据复制和类型检查等过程,理解装箱与拆箱能帮我们注意到各种容易引起性能消耗的陷阱。

2.1 装箱

将值类型转换为引用类型的过程,称为装箱。

值类型是存储在栈上,而引用类型的实际数据是存储在堆上。当一个值类型要转换成引用类型,首先创建一个新的引用类型对象,需要在堆上分配内存,这个内存大小为栈上值类型数据的大小和引用类型自身额外元数据的占用(一个存储类型标识,和同步块索引的对象头);然后将栈上值类型的值复制到堆上的装箱对象中;最后在栈上开辟一个空间存储这个新的引用类型对象的引用地址。

值类型到引用类型的装箱中,堆上的装箱对象与原栈上的值类型是相互独立的。它们复制的是值本身,修改原变量不会影响装箱对象,反之亦然。

值得注意的是装箱是隐式的,编译器会自动帮我们转换。也就是说我们在敲代码的时候是不需要额外操作就能将一个值类型赋值给引用类型。而前面我们了解到值类型到引用类型,需要一次堆空间分配,然后是栈到堆的复制,最后是栈分配引用类型的引用。这些都是在不经意间增大程序的性能开销。

int num = 25;
object obj = num;       //发生装箱

2.2 拆箱

将装箱后的引用类型转换回原来的值类型的过程,称为拆箱。
比起装箱的隐式方便,拆箱的步骤要求较为严格。在每一次拆箱前都需要验证堆上的装箱对象是否确实是目标值类型的装箱结果。类型验证通关后将堆上装箱对象中的值复制回栈上。

引用类型到值类型的拆箱需要手动触发,通过显式类型转换完成。并且拆箱也是值复制,栈上的新的值类型变量与堆上的装箱对象之间是相互独立,修改新的值类型变量不会影响堆上旧的装箱对象,反之亦然。

int num = 25;
object obj = num;       //发生装箱
int unboxedNum = (int)obj;  //执行拆箱

2.3 如何避免不必要的装箱与拆箱

堆的分配速度远慢于栈上内存的分配,频繁的装箱会消耗额外时间等待堆内存分配。而且频繁装箱可能导致GC频繁触发,占用系统资源。拆箱的时候类型验证也会消耗CPU资源。装箱和拆箱的过程中都会设计到数据的值复制,大批量数据复制会导致程序性能变差。

了解清楚了堆、栈与装箱拆箱的机制后,下面我们来讨论几个解决性能影响的方案。

2.3.1 泛型集合

泛型的关键特性是在编译时为不同的类型参数生成具体的类型实例,而不是依赖object作为中间类型。比方说ArrayList和List< T>。
给ArrayList添加值,最终值是被装箱成object对象,读取值本身也会经历一次拆箱

ArrayList arrayList = new ArrayList();
arrayList.Add("int");   //装箱
arrayList.Add("byte");
arrayList.Add("float");
string str = (string)arrayList[0]; //拆箱

而使用泛型集合,泛型通过类型参数化和编译时才把类型具体化,让值类型能够直接被存储和操作,无需转换为object类型。避免了装箱和拆箱。

 List<string> list = new List<string>();list.Add("string");list.Add("int");string str = list[0];

2.3.2 泛型参数

C#中方法参数的传递方式默认是按值传递的。对于值类型,传递的是变量的副本,方法内部修改参数变量不会改变外部原始变量。对于引用类型传递的是引用的副本,方法内部通过这个引用副本可以修改对象的内容。但是如果一旦修改引用副本本身这个引用值,比如在方法内部将引用副本重新赋值一个新的对象,这样副本引用值对应的堆上引用就和原始对象对应的堆上引用不同。

当值类型作为参数传递给方法参数是object时,默认会按值传递。值变量先装箱为object,再将装箱对象的引用传入方法。

public void Print(object obj) {Console.WriteLine(obj);
}int num= 1;
Print(num);  //装箱

和上面的思路一样,使用泛型通过类型参数化和编译时才把类型具体化,让值类型能够直接被存储和操作,无需转换为object类型。避免了装箱和拆箱。

void Print<T>(T obj) where T : struct
{Console.WriteLine(obj);
}int num = 1;
Print<int>(num);  

总结

文章讲解了.NET 中托管堆与托管栈的特性与数据存储差异,深入剖析了装箱、拆箱的原理及性能损耗,理解内存机制以优化程序性能。并给出泛型集合、泛型方法来等避免不必要装箱拆箱的方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/96251.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/96251.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/96251.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人工智能学习:Transformer结构中的子层连接(Sublayer Connection)

Transformer结构中的子层连接(Sublayer Connection) 一、子层连接介绍 概念 子层连接(Sublayer Connection),也称为残差连接(Residual Connection),是Transformer模型中的一个关键设计,用于将多个子层(如自注意力层和前馈全连接层)组合在一起。它通过残差连…

解锁Roo Code的强大功能:深入理解上下文提及(Context Mentions)

在AI使用中&#xff0c;我们经常需要AI或AI工具描述代码中的某个具体部分。但如果工具能直接“看到”所指的代码、错误信息甚至终端输出&#xff0c;协作效率会不会大幅提升&#xff1f;这正是 Roo Code 的“上下文提及&#xff08;Context Mentions&#xff09;”功能所要实现…

第5篇、 Kafka 数据可靠性与容错机制

在分布式消息队列系统中&#xff0c;数据可靠性 与 容错能力 是核心指标。Kafka 作为高吞吐、可扩展的流式处理平台&#xff0c;依靠副本复制、Leader 选举和 ISR 机制&#xff0c;保证了在节点故障时消息依然能够可靠传输与消费。 &#x1f4da; 目录 理论基础 一、数据复制…

Excel表格如何制作?【图文详解】表格Excel制作教程?电脑Excel表格制作?

一、问题背景 在日常办公中&#xff0c;无论是统计数据、整理报表&#xff0c;还是记录信息&#xff0c;Excel表格都是必不可少的工具。 但对新手来说&#xff0c;打开Excel后面对空白的单元格&#xff0c;常常不知道从何下手——不知道怎么选表格范围、怎么加边框让表格显形、…

阿里兵临城下,美团迎来至暗时刻?

9月10日&#xff0c;赶在阿里巴巴成立26周年之际&#xff0c;高德地图推出了首个基于用户行为产生的榜单“高德扫街榜”&#xff0c;被定义为“阿里生活服务超级新入口”&#xff0c;试图重新构建一套线下服务的信用体系。 上线第二天&#xff0c;就有媒体报道称“使用高德扫街…

Android逆向学习(十一) IDA动态调试Android so文件

Android逆向学习&#xff08;十一&#xff09; IDA动态调试Android so文件 一、 写在前面 这是吾爱破解论坛正己大大的第12个教程&#xff0c;并且发现一个神奇的事情&#xff0c;正己大大的教程竟然没有第11个&#xff0c;感觉很奇怪 写这个博客的主要原因是希望提供一种新的解…

Django全栈班v1.03 Linux常用命令 20250911 下午

课程定位 命令行 ! 黑客专属。 这套视频带你从Linux小白到命令行大师&#xff0c;涵盖文件管理文本处理系统监控网络操作。 零基础也能30分钟掌握程序员必备的技能。 课程亮点 1、零基础友好&#xff1a;从最基础的ls&#xff0c;cd命令开始&#xff0c;循序渐进 2、实战导向&a…

离线应用开发:Service Worker 与缓存

引言&#xff1a;离线应用开发在 Electron 中的 Service Worker 与缓存核心作用与必要性 在 Electron 框架的开发实践中&#xff0c;离线应用开发是提升用户体验和应用可用性的关键技术&#xff0c;特别是使用 Service Worker 实现缓存和离线功能&#xff0c;结合 Node.js 处理…

英发睿能闯关上市:业绩波动明显,毅达创投退出,临场“移民”

撰稿|张君来源|贝多商业&贝多财经近日&#xff0c;四川英发睿能科技股份有限公司&#xff08;下称“英发睿能”&#xff09;递交招股书&#xff0c;报考在港交所上市。据贝多商业&贝多财经了解&#xff0c;英发睿能还于9月3日披露《整体协调人公告&#xff0d;委任&…

Elixir通过Onvif协议控制IP摄像机,ExOnvif库给视频流叠加字符

Elixir 通过 ExOnvif 库&#xff0c;Onvif 协议可以控制IP摄像机等设备&#xff0c;这篇文章记录&#xff1a;使用ExOnvif库&#xff0c;给视频流叠加文字&#xff0c;使用ExOnvif库的接口模块&#xff1a;ExOnvif.Media、ExOnvif.Media2。 ExOnvif官方文档 此文章内容&#xf…

线程安全相关的注解

主要有下面三个加在类上的线程安全相关的注解。一.Immutable标记一个类为不可变的。这意味着该类的实例在构造完成后&#xff0c;其状态&#xff08;数据&#xff09;永远不能被更改。实现不可变性的严格条件&#xff08;Java内存模型中的定义&#xff09;&#xff1a;所有字段…

基于Springboot + vue3实现的在线智慧考公系统

项目描述本系统包含管理员、教师、用户三个角色。管理员角色&#xff1a;用户管理&#xff1a;管理系统中所有用户的信息&#xff0c;包括添加、删除和修改用户。配置管理&#xff1a;管理系统配置参数&#xff0c;如上传图片的路径等。权限管理&#xff1a;分配和管理不同角色…

赋能高效设计:12套中后台管理信息系统通用原型框架

中后台管理信息系统是企业数字化转型的核心引擎&#xff0c;肩负着提升运营效率、赋能精准决策的重任。面对多样化的业务场景和复杂的逻辑需求&#xff0c;如何快速、高质量地完成系统设计与原型构建&#xff0c;成为产品、设计与开发团队共同面临的挑战。 为此&#xff0c;一套…

LangGraph中ReAct模式的深度解析:推理与行动的完美融合——从理论到实践的智能Agent构建指南

在人工智能的演进历程中&#xff0c;ReAct&#xff08;Reasoning and Acting&#xff09;模式无疑是最具革命性的突破之一。它不仅仅是一种技术实现&#xff0c;更是对智能Agent思维模式的深刻重构。而LangGraph&#xff0c;作为这一理念的优秀实践者&#xff0c;将ReAct模式演…

蜂窝物联网模组在换电柜场景的发展前景分析

蜂窝物联网模组在换电柜场景中正迎来爆发式增长机遇&#xff0c;特别是在Cat.1技术路线主导的市场格局下&#xff0c;其应用价值已从基础通信服务拓展至安全监测、智能管理、电池溯源等核心领域&#xff0c;成为换电柜行业标准化、智能化升级的关键技术支撑。随着2025年新国标全…

机器学习之K折交叉验证

为了更好的评估机器学习训练出模型的泛化能力&#xff0c;即避免模型在训练集上表现良好&#xff0c;但在未见过的数据上表现不佳&#xff08;即过拟合&#xff09;&#xff0c;同时也减少了单一训练/测试集划分带来的随机性影响。一、什么是K折交叉验证&#xff1f;1、将数据集…

详细解读k8s的kind中service与pod的区别

Pod 是运行应用实例的“容器”&#xff0c;而 Service 是访问这些 Pod 的“稳定网络门户”。Pod&#xff08;容器组&#xff09;1. 核心概念&#xff1a; Pod 是 Kubernetes 中可以创建和管理的最小、最简单的计算单元。一个 Pod 代表集群上正在运行的一个工作负载实例。2. 职责…

python---PyInstaller(将Python脚本打包为可执行文件)

在Python开发中&#xff0c;我们常需要将脚本分享给不熟悉Python环境的用户。此时&#xff0c;直接提供.py文件需要对方安装Python解释器和依赖库&#xff0c;操作繁琐。PyInstaller作为一款主流的Python打包工具&#xff0c;能将脚本及其依赖打包为单个可执行文件&#xff08;…

利用归并算法对链表进行排序

/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };这里是链…

论文阅读_大模型情绪分析预测股票趋势

英文名称&#xff1a;Stock Price Trend Prediction using Emotion Analysis of Financial Headlines with Distilled LLM Model 中文名称&#xff1a;利用蒸馏大型语言模型对财务新闻标题情绪分析以预测股价趋势 链接: https://dl.acm.org/doi/pdf/10.1145/3652037.3652076作…