Skip to main content

存储栈与堆

· 4 min read
cun zhang
Docusaurus maintainer

这是学习 C# 时最容易产生误解的地方。

很多教材都会说:

  • 值类型变量存储在栈(Stack)中。
  • 引用类型对象存储在堆(Heap)中。

这句话虽然方便初学者理解,但实际上并不严谨。要真正理解值类型和引用类型,首先必须明白什么是栈,什么是堆。


什么是内存栈(Stack)?

栈(Stack)是一块专门用于管理方法调用和局部变量的内存区域。

它具有三个特点:

① 自动管理​

当一个方法开始执行时,CLR 会给这个方法分配一块栈空间。

例如:

void Test()
{
int a = 10;
int b = 20;
}

执行到这里时,栈里会创建一个栈帧(Stack Frame)。

里面保存:

┌──────────────┐
│ b = 20 │
├──────────────┤
│ a = 10 │
└──────────────┘

方法结束:

Test();

返回之后:

整个栈帧立即销毁

无需 GC(垃圾回收)。


② 后进先出(LIFO)​

Stack 的意思就是:

一摞盘子。

只能:

放在最上面
取也从最上面取

所以:

Main()
↓
Test()
↓
Print()

栈中就是:

┌──────────────┐
│ Print() │
├──────────────┤
│ Test() │
├──────────────┤
│ Main() │
└──────────────┘

Print 返回:

┌──────────────┐
│ Test() │
├──────────────┤
│ Main() │
└──────────────┘

所以叫:

后进先出(Last In First Out)


③ 分配速度极快​

为什么快?

因为栈根本不用找空闲内存。

只需要:

栈顶指针 += 大小

例如:

栈顶
↓

□□□□□□□□□□

申请一个 int(4 字节):

栈顶
↓

■■■■□□□□□□

仅仅移动一下指针即可。

因此:

栈的分配和释放速度几乎是所有内存分配方式中最快的。


什么是内存堆(Heap)?

堆(Heap)是一块专门存放对象(Object)数据的大型内存区域。

例如:

Person p = new Person();

执行:

new Person()

CLR 会在 Heap 中寻找一块足够大的连续空间。

例如:

Heap

┌──────────────┐
│ Person对象 │
│ Name │
│ Age │
└──────────────┘

然后返回:

Person对象的地址

变量:

p

保存的就是:

Person对象的位置(引用)

例如:

Stack Heap

┌────────┐ ┌──────────────┐
│ p ─────┼────────► │ Person对象 │
└────────┘ │ Name │
│ Age │
└──────────────┘

为什么需要堆?

因为对象大小通常不知道。

例如:

string s;

字符串长度可能:

"A"

也可能:

"Hello"

也可能:

100000 个字符

数组更明显:

int[] arr = new int[1000000];

长度运行时才知道。

所以:

不能把所有对象都直接放在固定大小的栈帧中。

因此:

CLR 将对象放到更灵活、更大的 Heap 中。


堆为什么慢?

因为需要:

  1. 找空闲区域
  2. 分配对象
  3. 维护对象信息
  4. 将来还要 GC 回收

不像栈:

移动一下栈顶指针即可。

所以:

Heap 的分配速度远慢于 Stack。


栈和堆最大的区别

内存栈(Stack)内存堆(Heap)
存放方法调用信息存放对象数据
自动分配动态分配
自动释放垃圾回收(GC)
速度快相对较慢
生命周期随方法结束生命周期由对象是否仍被引用决定

需要纠正一个常见误区

很多书都会说:

值类型存在栈中,引用类型存在堆中。

这句话并不准确。

更准确的说法应该是:

  • 值类型变量保存的是数据本身。
  • 引用类型变量保存的是对象的引用(地址信息)。
  • 对象通常分配在堆上,但变量本身放在哪里,取决于它声明的位置。

例如:

class Person
{
public int Age;
}

void Test()
{
Person p = new Person();
}

通常情况下:

Stack Heap

┌────────┐ ┌─────────────┐
│ p ─────┼────────► │ Person对象 │
└────────┘ │ Age = 0 │
└─────────────┘

这里:

  • p 是一个局部变量,因此位于当前方法的栈帧(逻辑上如此,JIT 可能优化到寄存器)。
  • Person 对象位于堆中。

再例如:

class Person
{
public int Age;
}

Person p = new Person();

这里的 Age 是 Person 对象的一部分,因此 Age 虽然是值类型,也存储在 堆中的对象内部,而不是栈上。

因此,对于 C# 来说,更重要的是理解:

  • 值类型决定的是变量保存的是数据本身。
  • 引用类型决定的是变量保存的是对象的引用。
  • 栈(Stack)和堆(Heap)描述的是内存区域和对象的存放方式,它们与“值类型/引用类型”有关联,但不是一一对应的关系。

一句最准确、也最适合教学的总结是:

栈(Stack)是 CLR 用来管理方法调用和局部变量的一块高速内存区域;堆(Heap)是 CLR 用来存放对象实例的一块动态内存区域。值类型和引用类型描述的是数据的存储语义,而不是简单地决定数据一定放在栈或堆中。