C#中的垃圾回收
GC:只负责托管堆;栈不是 GC 回收对象,但栈上的引用是 GC 的“根”。
Q1.栈和堆,这是什么?
你运行一个 C# 程序时:
操作系统会为你的程序创建一个 进程
操作系统会给这个进程分配一块 虚拟内存空间
栈和堆都在这块进程的虚拟内存里
👉 它们不是在“CPU里”,也不是在“磁盘里”,而是在 RAM(物理内存)映射的虚拟地址空间中。
栈
当你创建线程:new Thread(...).Start();
操作系统会:
为这个线程分配一块固定大小的内存(比如默认 1MB)这块内存就是这个线程的栈
所以:✔ 主线程一个栈✔ 每个新线程一个独立栈
堆
当 .NET CLR 启动时:
CLR 向操作系统申请一大块内存这块内存作为“托管堆”
当new一个对象时:var obj = new MyClass();
CLR 做:
1.在托管堆中找到一块足够大的连续空间,并移动一个“堆指针”
2.初始化对象
3.返回引用地址
注意:
正常情况下,堆分配其实也很快(只是移动指针),并不慢。
二者区别
| 特性 | 栈 | 托管堆 |
|---|---|---|
| 管理者 | 操作系统 + CPU | CLR (GC) |
| 是否自动释放 | 是(方法结束) | 否(GC判断) |
| 是否有碎片 | 无 | 有 |
| 是否需要GC | 不需要 | 需要 |
| 是否线程共享 | 否 | 是 |
GC 运行时到底发生了什么?
当内存压力大时:
- CLR 暂停所有线程(Stop The World)
- 从 GC Roots 开始扫描:
- 栈
- 静态字段
- CPU寄存器
- 标记所有可达对象
- 清理不可达对象
- 压缩堆(移动对象)
- 更新所有引用
例子
1 | // stage1 |
Foo 执行完之后:
- 栈发生了什么?
- 堆发生了什么?
- GC 会立刻回收 List 吗?
- 什么时候才会真正释放内存?
Ans:
Foo 方法结束后,list 引用从栈上消失,对应的 List 和其内部数组对象变为不可达对象。
这些对象仍驻留在托管堆的 Gen0 区域,直到下一次 GC 运行时被标记为不可达并回收。
1 | // stage2 |
再回答一下上述问题?
分析:
- 第一步 静态区有一个引用变量 它当前是 null 堆里没有 List 对象
static List<int> globalList; - 第二步 调用Foo() 堆里创建一个 List 对象,以及一个内部数组对象,globalList 引用指向它
此时结构:1
2
3GC Root(静态字段)
↓
globalList ----> List对象 ----> int[]数组 - GC 判断对象是否回收,只看:是否能从 GC Roots 走到它
- 因此:只有当:
globalList = null;或者AppDomain 卸载或者进程结束才会变为不可达
Ans: - globalList 是 GC Root
- Foo 结束后对象仍然可达
- 不会被回收
- 直到 globalList 被设为 null 或进程结束分析:
1
2
3
4
5
6
7
8
9// stage3
static List<int> globalList;
void Foo()
{
var temp = new List<int>();
globalList = temp;
temp = null;
} - stage1 创建对象
var temp = new List<int>();
结构:1
2
3GC Root(栈 temp)
↓
List对象 → int[] - stage2 赋值给 static
globalList = temp;
结构:此时两个 Root 指向同一个对象。1
2
3
4GC Root(栈 temp)
GC Root(static globalList)
↓
List对象 → int[] - stage3
temp = null;
现在:
栈上的 temp 不再指向对象,但 static 仍然指向对象
结构:对象仍然可达。因此不会被回收1
2
3GC Root(static globalList)
↓
List对象 → int[] - stage4 Foo 结束
栈帧弹出,temp 消失,但 static 仍然存在
结构维持:只要还有一个 Root 在指向它,它就活着。1
2
3GC Root(static globalList)
↓
List对象 → int[]
感谢阅读。