65.9K
CodeProject 正在变化。 阅读更多。
Home

关于 .NET 垃圾回收的超简化解释

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (50投票s)

2018年7月15日

CPOL

6分钟阅读

viewsIcon

35390

本文进行了超简化,介绍了 .NET 垃圾回收,其中故意省略了大量技术细节。它旨在提供一个典型的 C# 开发者日常工作中实际需要的基线理解水平。

垃圾回收常常是许多性能问题的根源(请原谅这个双关语),这很大程度上是由于误解造成的,所以请在阅读本文后花时间加深您的理解。

本文进行了超简化,介绍了 .NET 垃圾回收,其中故意省略了大量技术细节。它旨在提供一个典型的 C# 开发者日常工作中实际需要的基线理解水平。我当然不会通过提及堆栈、值类型、装箱等来增加复杂性。

免责声明:.NET 垃圾回收器是一个动态的复杂系统,它会适应您的应用程序,并且其实现经常会发生变化。

面试时开发者会说的话

垃圾回收器会自动查找不再使用的对象并释放内存。这有助于避免因程序员错误而导致的内存泄漏。

这足以找到工作,但不足以构建优秀、高性能的 C# 代码,相信我。

托管内存(简而言之)

.NET CLR(公共语言运行时)为您的应用程序预留了一块内存,用于管理应用程序创建的任何对象。当应用程序不再需要这些对象时,它们会被解除分配。这部分由垃圾回收器 (GC) 处理。

GC 可以在需要时扩展段大小,并且确实会这样做,但其首选方法是通过其分代垃圾回收来回收空间……

分代 .NET 垃圾回收器

新的、小的对象进入第 0 代。当发生一次回收时,任何不再使用(没有引用指向它们)的对象将被释放内存(解除分配)。任何仍在使用的对象将**存活**下来并被**提升**到下一代。

长生不老(或永远活着)并繁荣昌盛

问任何 .NET 专家,他们都会告诉你同样的话——一个对象应该短暂存在,否则就永远存在。我不会深入探讨性能细节——这是我希望你唯一能带走的一条规则。

为了理解这一点,我们需要回答:**为什么是分代?**

在一个设计良好的 C# 应用程序中,典型的对象将在不被提升出第 0 代的情况下生与死。我想到的是像这样的操作:

  • 局部变量,在运行时间很短的方法中使用
  • 为 Web API 调用生命周期而实例化的对象

第 1 代是“中间代”,它会捕获任何未成功从第 0 代晋升的、本应短暂存在于对象,并且回收过程仍然相对快速。

检查哪些对象未被使用会消耗资源并暂停应用程序线程。GC 在代际之间越往上,成本就越高,因为对某一特定代进行回收时,还必须回收其之前的所有代,例如,如果回收第 2 代,那么第 1 代和第 0 代也必须被回收(参见上面的图)。这就是为什么我们经常将第 2 代称为完全 GC。此外,寿命更长的对象通常更复杂清理!

不过不用担心——如果我们知道哪些对象可能会活得更长,我们可以不那么频繁地检查它们。考虑到这一点:

.NET GC 对第 0 代的运行频率最高,对第 1 代次之,对第 2 代的运行频率最低。

如果一个对象到达了第 2 代,那一定是有充分理由的——比如它是一个永久的、可重用的对象。如果对象意外地到达那里,它们会长时间占用内存,并导致更多那些庞大的第 2 代完全回收!

但是代际都只是表面功夫!

在通过性能分析器和调试器探索应用程序,初次查看 GC 时,最大的陷阱是大型对象堆 (LOH) 也被称为第 2 代。

实际上,您的对象将进入托管堆段(在前面提到的分配给 CLR 的内存中)。

小对象将连续添加到小对象堆 (SOH) 的第 0 代,从而避免寻找空闲空间。为了减少对象消亡时的碎片,堆可能会被压缩。

下面是一个简化的第 0 代回收,然后是第 1 代回收,中间有新的对象分配(这是我第一次尝试这样的动画)。

点击动画可打开视频,您可以在视频中暂停等。



大对象存储在大型对象堆上,不会自动压缩,但会尝试重用空间

从 .NET451 开始,您可以指示 GC 在下一次回收时进行压缩。但对于处理 LOH 碎片,最好选择其他选项,例如池化可重用对象。

大对象有多大?

众所周知,>= 85KB 的对象就是大对象(或 1000 个双精度浮点数的数组)。您需要了解更多……

您可能在想您处理过的那个大型 Bitmap 图像——实际上那个对象使用了 24 字节,而 Bitmap 本身是在非托管内存中。真正很少看到非常大的对象。更典型的情况是,大对象会是一个数组。

在下面的示例中,`LargeObjectHeapExample` 中的对象实际上是 16 字节,因为它只由通用类信息和指向 `string` 和字节数组的指针组成。

通过实例化 `LargeObjectHeapExample` 对象,我们实际上在堆上分配了**3 个对象**:其中 2 个在小对象堆上;字节数组在大型对象堆上。

还记得我之前关于大型对象堆中对象的说法吗——注意字节数组如何报告为处于第 2 代!LOH 在逻辑上被包含在第 2 代的一个原因是,大对象通常有更长的生命周期(回想一下我之前关于分代 GC 中寿命较长的对象的说法)。另一个原因是在进行早期代际中发生的压缩时,复制大对象的**成本**。

什么会触发回收?

  • 尝试分配的对象超过了某一特定代或大型对象堆的阈值
  • 调用 `GC.Collect`(这将在另一篇文章中讨论)
  • 操作系统发出低系统内存信号

请记住,第 2 代和 LOH 在逻辑上是同一件事,因此触及其中任何一个的阈值都会触发两个堆的完全(第 2 代)回收。这是需要考虑的性能问题(本文未涉及)。

摘要

  • 对某一特定代进行回收也会回收其下方所有代,即回收第 2 代也会回收第 1 代和第 0 代。
  • GC 会将回收后仍然存活(因为它们仍在被使用)的对象提升到下一代。尽管如此,请参考上一条——不要指望在第 0 代回收发生时,第 1 代中的对象会移动到第 2 代。
  • GC 对第 0 代的运行频率最高,对第 1 代次之,对第 2 代的运行频率最低。考虑到这一点,**对象应该短暂存在(最好在第 0 代或第 1 代中消亡),或者(当然是故意的)永远存在于第 2 代**。

历史

  • 20/08/2018 - 修复了导入格式问题,并添加了双精度浮点数数组阈值以保持完整性
© . All rights reserved.