【C#】理解.NET內存機制:堆、棧與裝箱拆箱的底層邏輯及優化技巧

文章目錄

  • 前言
  • 一、棧與堆
    • 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作…