每个线程对应一个stack,线程创建的时候CLR为其创建这个stack,stack主要作用是记录函数的执行情况。值类型变量(函数的参数、局部变量 等非成员变量)都分配在stack中,引用类型的对象分配在heap中,在stack中保存heap对象的引用指针。GC只负责heap对象的释放,heap内存空间管理。

除去pinned object等影响,heap中的内存分配很简单,一个指针记录heap中分配的起始地址,根据对象大小连续的分配内存。
每个函数调用时,逻辑上在thread stack中会产生一个帧(stack frame),函数返回时对应的stack frame被释放掉。
用个简单的函数查看执行时CLR对栈的处理情况:
1static void Main(string[] args)
2{
3 int r = Sum(2, 3, 4, 5, 6);
4}
5private static int Sum(int a, int b, int c, int d, int e)
6{
7 return a + b + c + d + e;
8}JIT编译后主要汇编代码如下(其他的情况下汇编代码可能有所差别,但用这个简单函数大致看下栈的管理已经足够):
1;====函数Main====
2push 4 ;第3个参数到最后一个参数压栈
3push 5
4push 6
5mov edx,3 ;第1、第2个参数分别放入ecx、edx寄存器
6mov ecx,2
7call dword ptr ds:[00AD96B8h] ;调用函数Sum,执行call的时候返回地址(即下面这条mov语句的地址)自动压栈了
8mov dword ptr [ebp-0Ch],eax ;将函数返回值设置到局部变量r中(函数调用结束返回值在eax寄存器中)
9
10;====函数Sum====
11push ebp ;保存原始ebp寄存器
12mov ebp,esp ;将当前栈指针保存在ebp中,后面使用ebp对参数和局部变量寻址
13sub esp,8 ;分配两个局部变量
14mov dword ptr [ebp-4],ecx ;第1个参数放入局部变量
15mov dword ptr [ebp-8],edx ;第2个参数放入局部变量
16...... ;CLR的检查代码
17mov eax,dword ptr [ebp-4] ;a + b + c + d + e
18add eax,dword ptr [ebp-8] ;第1个参数+第2个参数(2+3)
19add eax,dword ptr [ebp+10h] ;+第3个参数(4)
20add eax,dword ptr [ebp+0Ch] ;+第4个参数(5)
21add eax,dword ptr [ebp+8] ;+第5个参数(6)
22mov esp,ebp ;恢复栈指针(局部变量被释放了)
23pop ebp ;恢复原始的ebp寄存器值
24ret 0Ch ;函数返回. 1: 返回地址自动出 栈; 2: esp减去0Ch(12个字节),即从栈中清除调用参 数; 3: 返回值在eax寄存器中执行时刻的stack状态如下(栈基地址为高端地址,栈顶为低端地址):

Stack状态变化过程:
这种调用约定类似__fastcall
结合引用类型变量、值类型的ref参数,下面代码简化的stack状态如下:
1public static void Run(int i)
2{
3 int j = 9;
4 MyClass1 c = new MyClass1();
5 c.x = 8;
6 int result = Sum(i, 5, ref j, c);
7}
8public static int Sum(int a, int b, ref int c, MyClass1 obj)
9{
10 int r = a + b + c + obj.x;
11 return r;
12}
13public class MyClass1
14{
15 public int x;
16}Stack状态:

任何时候引用类型都分配在heap中,在stack中只是保存对象的引用地址。Run函数执行完毕之后,heap中的MyClass1对象c成为可回收的垃圾对象,在GC时进行回收。