C# 内存模型深度解析
篇章:01-认知篇 |阅读时间:约 40 分钟 |前置知识:了解 C# 基本语法
一、引言
GC 管理的对象在 C# 的托管堆上分配。但并非所有数据都在托管堆上——值类型可以在栈上分配,引用类型的引用在栈上但对象在堆上。理解 C# 内存模型的每个细节,是准确分析 GC 行为的前提。
二、托管堆
托管堆是 GC 管理的内存区域,所有引用类型对象(class 实例、数组、委托、字符串)都分配在此。
托管堆布局: ┌────────────────────────────────────────────┐ │ Gen 0 │ Gen 1 │ Gen 2 │ LOH │ │ (新对象) │ (晋升) │ (长期存活) │ (大对象) │ └────────────────────────────────────────────┘关键特征:
- 堆上的对象通过可达性分析判断生死
- 对象被回收后内存可能被压缩(移动对象)
- 分配在堆上的对象具有不确定的生命周期
三、栈
每个线程有独立的栈,存储:
- 局部变量和方法参数
- 函数调用的返回地址
- 值类型实例(当作为局部变量时)
栈的自动管理特性:
- 分配快:只需移动栈指针(SP)
- 释放快:方法返回时 SP 恢复
- 不产生 GC:栈上的数据随方法返回自动释放
void Example() { int x = 42; // x 在栈上,不经过 GC MyClass obj = new(); // obj 引用在栈上,对象在堆上 // x 随方法返回自动释放,不产生 GC // obj 指向的堆对象由 GC 管理 }四、值类型与引用类型
| 维度 | 值类型(struct) | 引用类型(class) |
|---|---|---|
| 存储位置 | 栈(局部变量)或堆(装箱/字段) | 堆 |
| 赋值语义 | 复制整个数据 | 复制引用(指针) |
| GC 管理 | 不直接管理(栈上) | GC 标记-清扫 |
| 默认传递 | 传值(复制) | 传引用(指针复制) |
| 可为 null | 可(Nullable) | 可 |
| 多态 | 不支持(无虚函数表指针) | 支持 |
五、对象头与内存布局
每个托管对象从对象头开始,然后是实际数据:
┌─────────────────┐ │ Object Header │ 8-16 字节(同步块索引 + 方法表指针) ├─────────────────┤ │ 字段 1 │ │ 字段 2 │ │ ... │ 实际数据 │ 字段 N │ ├─────────────────┤ │ 填充 (Padding) │ 对齐到 4/8 字节 └─────────────────┘对象头的开销:
- 32 位:8 字节(SyncBlockIndex 4B + MethodTable* 4B)
- 64 位:16 字节(SyncBlockIndex 4B + 填充 4B + MethodTable* 8B)
六、GC Roots
GC 从一组称为 Roots 的引用出发遍历对象图:
| Roots 类型 | 内容 | 数量级 |
|---|---|---|
| 栈变量 | 每个线程栈上的对象引用 | 数百 |
| 静态变量 | 全局静态字段 | 数百到数千 |
| 寄存器 | CPU 寄存器中的引用 | 数十 |
| GC Handle | GCHandle.Alloc 固定的对象 | 数百 |
| Finalizer 队列 | 待执行 Finalizer 的对象 | 数十 |
七、总结
C# 内存模型的核心在于"栈引用 → 堆对象"的架构。栈上的值类型自动释放不经过 GC,堆上的引用类型由 GC 管理。理解这个基础架构是正确分析 GC 分配来源和优化策略的前提。
八、内存模型对 GC 性能的影响
| 内存区域 | GC 管理 | 分配速度 | 回收方式 | 性能影响 |
|---|---|---|---|---|
| 托管堆 | GC 标记-清扫 | 中(bump ptr) | GC 周期回收 | GC 暂停 |
| 栈 | 自动(SP移动) | 极快(1指令) | 函数返回 | 零暂停 |
| LOH | GC 特殊管理 | 慢(空闲链表) | Gen2 GC | 长暂停 |
| Pinned | GC + 手动 | 正常 | 阻碍压缩 | 增加碎片 |