.NET垃圾回收器(GC)是.NET框架中一个重要的组成部分,它负责自动管理内存的分配和释放。本文旨在为C#开发者提供一个关于.NET垃圾回收机制的简化理解,以便在日常工作中更好地进行内存管理。请注意,本文为了简化理解,省略了许多技术细节,比如栈、值类型、装箱等。
.NET CLR(公共语言运行时)为应用程序预留了一块内存区域,用于管理应用程序分配的所有对象。当应用程序不再需要这些对象时,它们会被垃圾回收器释放。垃圾回收器可以根据需要扩展内存段的大小,但它更倾向于通过分代垃圾回收来回收空间。
分代垃圾回收是.NET垃圾回收器的核心机制。新创建的小对象会被放入第0代。当发生垃圾回收时,任何不再被使用(没有引用)的对象的内存会被释放。仍然被使用的对象会被“提升”到下一代。
在设计良好的C#应用程序中,典型的对象会在没有被提升出第0代的情况下生存和死亡。例如,短时运行方法的局部变量、Web API调用的生命周期内的对象等。第1代是“中间代”,它会捕获任何逃出第0代的短命对象,并且它的收集速度相对较快。
检查哪些对象未被使用会消耗资源并暂停应用程序线程。随着代数的增加,垃圾回收的成本也会增加,因为收集特定代的对象时,也必须收集所有先前的代。例如,如果第2代被收集,那么第1代和第0代也必须被收集。这就是通常将第2代称为全GC的原因。此外,活得更久的对象清理起来往往更复杂。
.NET GC在第0代运行得最频繁,在第1代较少,在第2代更少。如果对象到达第2代,它需要有充分的理由——比如是一个永久的、可重用的对象。如果对象无意中到达那里,它们会停留更长时间,占用更多内存,并导致更多的第2代全GC!
但是,代数只是一个表象!当第一次通过分析器和调试器探索应用程序的GC时,最大的陷阱是大对象堆(LOH)也被称为第2代。
物理上,对象最终会出现在CLR分配的内存段中的托管堆段上。小对象会连续添加到小对象堆(SOH)的第0代,以避免寻找空闲空间。为了减少对象死亡时的碎片化,堆可能会被压缩。
在.NET 4.5.1及更高版本中,可以告诉GC在下一次收集时压缩它。但更倾向于使用其他选项来处理LOH碎片化,比如池化可重用对象。
一个众所周知的事实是,大于或等于85KB的对象是大对象(或1000个double数组)。需要了解更多...
可能在想那个处理过的大位图图像——实际上,该对象使用24字节,而位图本身在非托管内存中。真正大的对象很少见。更典型的是,大对象将是一个数组。
在以下示例中,来自LargeObjectHeapExample的对象实际上是16字节,因为它只由一般类信息和指向字符串和字节数组的指针组成。
通过实例化LargeObjectHeapExample对象,实际上在堆上分配了3个对象:其中2个在小对象堆上;字节数组在大对象堆上。
记住前面说的关于大对象堆中的东西——注意字节数组报告为在第2代!LOH在逻辑上位于第2代的一个原因是,大对象通常有更长的生命周期(回想一下前面说的在分代GC中活得更久的对象)。另一个原因是在执行早期代的压缩时复制大对象的成本。
触发收集的原因包括:尝试分配超出代的阈值、调用GC.Collect(将在另一篇文章中讨论)、操作系统信号低系统内存。
记住第2代和LOH在逻辑上是相同的,所以达到任一阈值都会触发两个堆的全(第2代)收集。这是性能方面需要考虑的(超出本文范围)。
总结:特定代的收集也会收集所有低于它的代,即收集2也会收集1和0。GC将存活的收集对象(因为它们仍在使用)提升到下一代。尽管看到前面的点——不要指望第1代的对象在第0代收集时移动到第2代。