C# 中的 GC 回收基础与注意事项
1. 什么是 GC(Garbage Collection)
GC(垃圾回收) 是 .NET CLR 提供的一种自动内存管理机制,用来回收不再被使用的托管对象。
简单理解:
当一个对象再也找不到引用它的路径时,GC 就会回收它。
特点:
- 自动执行,无需手动 free
- 只回收托管内存(Managed Heap)
- 并非实时执行
2. GC 如何判断对象是否需要回收
GC 的核心判断标准是:
对象是否还能从 GC Root 被“访问到(Reachable)”
如果不能访问到,则对象被认为是垃圾。
3. 什么是 GC Root
GC Root 是 GC 查找引用的起点,只要对象能从这些地方被引用,就不会被回收。
常见 GC Root 包括:
- 当前方法中的局部变量(栈上的引用)
- 静态变量(static 字段)
- 当前线程对象
- 全局单例
- 事件的发布者(非常重要)
- 非托管代码(如 Rhino / COM / C++)持有的对象
4. GC 回收的基本流程
- 从所有 GC Root 开始遍历
- 标记所有可达对象
- 未被标记的对象视为垃圾
- 释放其占用的内存
5. GC 的分代机制
为了提高性能,.NET 使用分代回收(Generational GC)。
| 代(Generation) | 说明 |
|---|---|
| Gen 0 | 新创建的对象 |
| Gen 1 | 存活过一次回收 |
| Gen 2 | 长时间存活的对象 |
说明:
- 大部分对象会在 Gen 0 被回收
- Gen 2 回收成本最高
- 长期被引用的 UI / Panel 对象通常会进入 Gen 2
6. GC 不会回收哪些东西
GC 不会回收以下内容:
- 正在被引用的对象
- 静态变量引用的对象
- 被事件订阅的对象
- 非托管资源(如文件句柄、GDI、显存)
7. 事件与 GC(最常见的内存泄漏来源)
在 C# 中:
事件 = 发布者持有订阅者的强引用
示例:
class Publisher { public static event Action OnEvent; } class Subscriber { public Subscriber() { Publisher.OnEvent += Handle; } void Handle() { } }
问题:
- Publisher 是 static(GC Root)
- Subscriber 被事件引用
- 即使外部不再使用 Subscriber,GC 也无法回收
解决方法:
- 在对象生命周期结束时必须使用 -= 取消订阅
8. UI / Panel 中的 GC 注意事项
UI 对象通常具有:
- 生命周期短
- 创建 / 销毁频繁
- 但容易被全局对象引用
常见问题来源:
- static 事件
- 文档级事件(如 RhinoDoc)
- 单例管理器
推荐做法:
- PanelShown 时订阅事件
- PanelHidden / PanelClosing 时取消订阅
9. IDisposable 与 GC 的关系
GC 不会自动释放非托管资源。
对于以下资源:
- 文件
- Socket
- GDI / 显卡资源
- Rhino / COM 对象
必须实现 IDisposable:
class MyResource : IDisposable { public void Dispose() { // 手动释放非托管资源 } }
使用方式:
using (var res = new MyResource()) { // 使用资源 }
10. Finalizer(析构函数)
Finalizer(~ClassName):
- 由 GC 在不确定时间调用
- 性能成本高
- 不保证立即执行
不推荐滥用:
~MyClass() { // 不可靠,且影响性能 }
推荐使用:
- IDisposable + using
- 明确的资源释放时机
11. GC.Collect() 的正确认知
GC.Collect():
- 强制触发 GC
- 会导致性能下降
- 破坏 GC 的优化策略
结论:
- ❌ 不推荐在业务代码中调用
- ✅ 只在调试或内存分析场景下使用
12. 常见 GC 误区
- C# 有 GC 就不会内存泄漏(错误)
- 对象设为 null 就一定会回收(错误)
- UI 关闭等于对象销毁(错误)
- GC 会处理事件引用(错误)
13. 实践总结
什么时候必须关注 GC:
- 插件开发(Rhino / Revit / Unity)
- 长时间运行的程序
- 使用 static / 单例 / 全局事件
- UI 面板频繁创建和销毁
核心原则一句话:
让对象在不需要时,从所有 GC Root 中“断开引用”