你必须理解的魔法
\n\n每个写过C#至少几周的人都知道:内存由运行时环境(CLR)管理。你只需通过new创建对象,当它们不再需要时——垃圾回收器(GC)就会过来清理一切。听起来像魔法,对吧?但在这魔法背后是严谨的工程学,如果不理解它——你的应用程序就会开始“卡顿”、“吞噬”数GB内存,甚至因OutOfMemoryException而崩溃。
在本文中,我们将深入CLR底层,详细了解C#内存管理的工作原理。你将了解栈与堆的区别、什么是对象代龄、为什么IDisposable是你最好的朋友,以及如何避免踩到大对象堆的坑。开始吧!
栈与堆:教科书里避而不谈的基础
\n\n当你的程序运行时,CLR将内存分为两个主要区域:栈(Stack)和堆(Heap)。理解它们之间的区别是基础,没有这个基础,其他一切都会失去意义。
\n\n栈——快速但容量小
\n栈是存储值类型(int、bool、struct、enum)和对象引用的内存区域。它遵循LIFO(后进先出)原则。当你调用一个方法时,会为其在栈中分配一个帧。方法一旦结束,该帧立即被销毁。这里不需要任何垃圾回收器。栈的访问速度最快,但大小有限(通常每个线程1-4 MB)。
\n\nvoid Example()\n{\n int x = 42; // x 在栈中\n string s = "Привет"; // 引用 s 在栈中,对象本身在堆中\n}\n\n\n堆——较慢但容量更大
\n堆(托管堆)是“长期”内存。所有引用类型(类、字符串、数组、委托)都存放在这里。每次使用new都会在堆中分配内存。这时垃圾回收器就登场了。正是它决定何时可以释放内存,何时需要等待。
重要提示:栈并非“不好”,堆也并非“好”。它们各有各的用途。值类型速度快,但不能在没有ref关键字的情况下通过引用传递。引用类型灵活,但需要GC管理。
C#垃圾回收器的工作原理:代龄与算法
\n\n.NET中的垃圾回收器不仅仅是“删除并遗忘”。它是一个智能机制,利用代龄(Generations)来最小化应用程序的暂停时间。
\n\n对象的三个代龄
\n- \n
- Gen 0 —— 新生对象。生命周期短。Gen 0的回收频繁且非常快速。 \n
- Gen 1 —— 经过一次回收后“幸存”的对象。是年轻对象和年老对象之间的缓冲区。 \n
- Gen 2 —— 长寿对象。Gen 2的回收最昂贵且最罕见。如果一个对象存活到了Gen 2,它很可能会长期留在那里。 \n
GC的工作算法简化如下:
\n- \n
- 应用程序请求内存。如果堆中没有空间,则触发回收。 \n
- GC从根(全局静态字段、线程栈中的局部变量、CPU寄存器)开始构建对象可达性图。 \n
- 所有不可达对象被标记为“垃圾”。 \n
- 进行压缩(compaction)——移动存活对象以避免碎片化。 \n
- 释放的内存返回池中。 \n
以下是一个直观示例,展示GC如何影响性能:
\n\nclass HeavyObject\n{\n public byte[] Data = new byte[1024 * 1024]; // 1 MB\n}\n\nvoid TestGC()\n{\n for (int i = 0; i < 1000; i++)\n {\n var obj = new HeavyObject(); // 每次在堆中分配1 MB\n // obj 超出作用域——变成垃圾\n }\n // 循环结束后,GC可能会回收Gen 0