栈和堆在 C# 中的内存分配位置不同
栈(Stack)用于存储值类型变量、方法参数、局部变量和方法调用帧,由编译器自动管理,生命周期严格遵循后进先出(LIFO);堆(Heap)用于存储引用类型对象(如
class实例、数组、字符串等),由 .NET 运行时的垃圾回收器(GC)管理,生命周期不固定。
关键区别不在“快慢”,而在“谁负责释放”:栈上内存随作用域退出自动弹出,无需 GC 干预;堆上对象只有在 GC 判定为不可达后才可能被回收,且时机不可控。
ref struct
为什么必须放在栈上
ref struct是 C# 7.2 引入的特殊类型(如
Span<t></t>、
ReadOnlySpan<t></t>),设计初衷就是禁止逃逸到堆——编译器会直接拒绝任何可能导致其被装箱、作为字段存入 class、或被捕获进 lambda 的写法。
常见报错:
Cannot declare a variable of type 'Span<int>' in a context where it may be lifted to an anonymous method or lambda expression</int>,本质是编译器在做栈逃逸检查。 不能作为
class的字段 不能实现任何接口(接口变量会引发装箱) 不能用在
async方法中(因为状态机会生成堆上的状态机类)
值类型不一定都在栈上
很多人误以为
struct总在栈上,其实只看“声明位置”:局部
struct变量通常在栈上;但一旦它成为引用类型对象的字段(比如
class A { public Point p; }),那 p就随
A实例一起分配在堆上;同理,
struct数组元素也全部在堆上。
验证方式:用
unsafe+
fixed或
System.Runtime.CompilerServices.Unsafe.AsPointer查看地址,你会发现同一
struct类型在不同上下文里地址段完全不同。
unsafe
{
int x = 42;
Console.WriteLine($"stack addr: {(long)Unsafe.AsPointer(ref x)}"); // 通常高位地址(栈向下增长)
<pre class='brush:php;toolbar:false;'>var arr = new int[1];
Console.WriteLine($"heap addr: {(long)Unsafe.AsPointer(ref arr[0])}"); // 通常低位地址(堆向上增长)}
GC 不管栈,但栈溢出照样崩
栈空间由操作系统在线程创建时分配(默认 Windows 是 1MB),深度递归、超大局部数组(如
int[1000000])、或无限嵌套的
async状态机都可能触发
StackOverflowException——这个异常无法 catch,进程直接终止。
而堆内存不足会抛
OutOfMemoryException,此时 GC 会尝试回收,失败后才崩溃,还有调试窗口可抓内存快照。 避免在栈上分配大结构体(如含百万字节数组的
struct) 递归算法优先改造成迭代,尤其处理树/图等深层结构时
stackalloc分配的内存必须在当前作用域内使用,且不能返回给调用方指针
栈和堆不是性能高低的代名词,而是资源生命周期模型的选择。混淆它们最危险的地方,是以为“值类型=栈上=安全”,结果把大
struct塞进 class 字段或集合里,既没省内存,又让 GC 负担更重。
