C#中堆和栈的区别分析

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

使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。如果我们写出如下两段代码:
代码段1:

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

代码段2:

public class MyInt
{
public int MyValue;
}

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

问题1:你知道代码段1在执行的时候,pValue和result在内存中是如何存放,生命周期又如何?代码段2呢?
要想释疑以上问题,我们就应该对.Net下的栈(Stack)和托管堆(Heap)(简称堆)有个清楚认识,本立而道生。如果你想提高程序性能,理解栈和堆,必须的!
本文就从栈和堆,类型变量展开,对我们写的程序进行庖丁解牛。
C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成我们C#程序的运行环境。

一,栈 vs 堆:区别?

栈通常保存着我们代码执行的步骤,如在代码段1中 AddFive()方法,int pValue变量,int result变量等等。而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。

如图1,栈与堆示意图

 

(图1)

栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。


二,什么元素被分配到栈?什么被分配到堆?

当我们程序执行的时候,在栈和堆中分配有四种主要的类型:值类型,引用类型,指针,指令。

值类型:
在C#中,继承自System.ValueType的类型被称为值类型,主要有以下几种(CLR2.0中支持类型有增加):
* bool
* byte
* char
* decimal
* double
* enum
* float
* int
* long
* sbyte
* short
* struct
* uint
* ulong
* ushort

引用类型:
以下是引用类型,继承自System.Object:
* class
* interface
* delegate
* object
* string

指针:
在内存区中,指向一个类型的引用,通常被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显示使用。需要注意的是,一个类型的引用即指针跟引用类型是两个完全不同的概念。指针在内存中占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区才是我们真正的数据或者类型。如图2:


(图2)

指令:
后文对指令再做介绍。

三,如何分配?
我们先看一下两个观点:
观点1,引用类型总是被分配在堆上。(正确?)
观点2,值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上。(这个理解起来有点难度,需要慢慢来)

上文提及的栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。

下面我们以实例来详谈。

还是我们开篇所列的代码段1:

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

当AddFive方法开始执行的时候,方法参数(parameters)则在栈上分配。如图3:

(图3)

注意:方法并不在栈中存活,图示仅供参考。
接着,指令指向AddFive方法内部,如果该方法是第一次执行,首先要进行JIT即时编译。如图4:


(图4)

当方法内部开始执行的时候,变量result被分配在栈上,如图5:


(图5)

方法执行完毕,而且方法返回后,如图6所示:

(图6)

在方法执行完毕返回后,栈上的区域被清理。如图7:

(图7)

以上看出,一个值类型变量,一般会分配在栈上。那观点2中所述又做何理解?“值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上”。
原因就是如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。
还是代码段2:

public class MyInt
{
public int MyValue;
}

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

当线程开始执行AddFive方法的时候,参数被分配到栈上,如图8所示:

(图8)
由于MyInt是一个引用类型,所以它被分配到堆上,并且在栈中生成一个指针(result),如图9:

(图9)
AddFive方法执行完毕时的情况如图10:

(图10)

栈上内存被清理,堆中依然存在,如图11:

 
(图11)

当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有不可达到对象,即无被引用的对象,进行清理。并通知栈中的指针重新指向地址排序后的对象。现在我们应该知道,了解栈和堆,对我们开发出高性能程序的重要性。当我们使用引用类型的时候,一般是对指针进行的操作而非引用类型对象本身。但是值类型则操作其本身。
接下来,我们用例子说明这一点。

例1:

public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
执行结果为3,稍作修改:

例2:

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。

我们来分析下原因,其实例1的跟以下代码所起效用一样:

public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}

如图12所示,在栈上x和y分别占用一块内存区,互不干扰。

(图12)

而例2,与以下代码所起效用一样:

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

如图13所示,


<think>嗯,用户问的是C#中如何理解内存的差异。我需要先回顾一下C#的内存管理机制,的基本概念。首先,(Stack)(Heap)都是内存中的区域,但用途管理方式不同。 通常用于存储方法的调用局部变量,特别是值类型变量。的内存分配释放是自动的,遵循后进先出的原则,方法结束时帧弹出,变量自动释放。而用于存储引用类型的对象,比如类实例、数组等。的内存由垃圾回收器(GC)管理,当对象不再被引用时,GC会回收内存。 用户可能已经了解一些编程基础,但需要明确C#中的具体实现。他们可能对值类型引用类型的存储位置有疑问,或者在实际编程中遇到了内存管理的问题,比如内存泄漏或性能问题。需要解释清楚区别,以及它们如何影响程序的行为。 应该先定义,然后对比它们的特点,比如分配方式、生命周期、访问速度等。再结合C#的具体例子,比如int是值类型,存在上;而string是引用类型,存在上。同时,需要提到装箱拆箱操作,因为这会涉及到之间的数据转换。 另外,可能需要指出一些常见的误区,比如结构体(struct)是值类型,通常存储在上,但如果作为类的成员,就会存储在上。这有助于用户更准确地理解内存分配。 还要考虑用户的实际应用场景,比如性能优化时,理解区别可以帮助减少GC的压力,提高程序效率。此外,调试内存问题时,知道变量存储的位置有助于快速定位问题。 最后,总结关键点,强调正确管理内存的重要性,并给出学习资源或进一步的学习建议,比如推荐阅读CLR via C#这样的书籍,或者官方文档中的内存管理部分。</think>在C#中理解内存(Stack)(Heap)的差异是掌握内存管理的关键,以下是简明技术解析: --- ### **核心差异对比** | 特性 | (Stack) | (Heap) | |---------------------|--------------------------------------|-------------------------------------| | **存储对象** | 值类型变量、方法参数、方法调用帧 | 引用类型对象(类实例/数组/字符串等) | | **生命周期** | 随方法结束自动释放 | 由GC垃圾回收器管理 | | **分配速度** | 快速(指针移动即可分配) | 较慢(需寻找可用内存块) | | **空间限制** | 较小(默认1MB,可配置) | 较大(受进程内存限制) | | **线程独占** | 每个线程独立空间 | 所有线程共享空间 | --- ### **C#具体表现** #### 1. **值类型(典型)** ```csharp void Example() { int a = 10; // 分配 DateTime dt = DateTime.Now; // 结构体(值类型)也存储在 } ``` - 方法结束时自动弹出帧,变量立即释放 - **例外**:当值类型被装箱(boxing)时会进入 ```csharp object boxedInt = 42; // 装箱操作,值类型数据进入 ``` #### 2. **引用类型(典型)** ```csharp class MyClass { public int Value; } void Example() { MyClass obj = new MyClass(); // 对象在中分配 int[] arr = new int[10]; // 数组在中分配 } ``` - `obj`变量本身(引用指针)存储在,指向中实际对象 - GC通过可达性分析回收未被引用的对象 --- ### **内存交互示例** ```csharp void ProcessData() { int stackValue = 5; // 存储 var heapObject = new DataModel(); // 存储 // 修改对象属性(通过上的引用操作数据) heapObject.Value = stackValue; } class DataModel { public int Value; } // 引用类型 ``` --- ### **常见误区** 1. **结构体(struct)不一定全在**: - 当结构体是类的成员时,随类实例存储在 - 当结构体被装箱时存储在 2. **字符串(string)特殊性**: - 虽然属于引用类型,但因不可变性可能被驻留(interned) --- ### **调试验证技巧** 1. **查看内存地址**(需不安全上下文): ```csharp int stackVar = 10; unsafe { int* ptr = &stackVar; Console.WriteLine($"地址: {(long)ptr:X}"); } var heapObj = new object(); GCHandle handle = GCHandle.Alloc(heapObj, GCHandleType.Pinned); Console.WriteLine($"地址: {handle.AddrOfPinnedObject():X}"); ``` --- ### **性能优化要点** 1. 减少分配(避免过度创建对象) 2. 注意闭包导致的意外分配 3. 大值类型(>16字节)建议使用`ref struct` 推荐延伸阅读: - 《CLR via C#》(深入讲解.NET内存模型) - 《Pro .NET Memory Management》(内存管理专项指南)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值