【C# .Net GC】垃圾回收算法 应用程序线程运行时,
阅读原文时间:2023年07月08日阅读:3

触发垃圾回收的条件

当满足以下条件之一时将发生垃圾回收:

  • 操作系统报告低内存请看(将触发第2代垃圾回收)。 这是通过 OS 的内存不足通知或主机指示的内存不足检测出来。

  • 由托管堆上已分配的对象使用的内存超出了可接受的阈值。 随着进程的运行,此阈值会不断地进行调整。触发第0代回收

  • 调用 GC.Collect 方法。 几乎在所有情况下,你都不必调用此方法,因为垃圾回收器会持续运行。 此方法主要用于特殊情况和测试。如果应用程序代码通过调用 GC.Collect 方法并将 generation 参数指定为 2 来包含回收。

  • 应用程序调用new操作符创建对象,发现没有足够的地址空间来分配对象,CLR就经行垃圾回收。

  • CLR卸载APPDomain,所有代0、1、2的垃圾回收。

  • CLR正在关闭,收回内存

何时收集大型对象?

通常情况下,出现以下三种情形中的任一情况,都会执行 GC:

  • 分配超出第 0 代或大型对象阈值。

    阈值是某代的属性。 垃圾回收器在其中分配对象时,会为代设置阈值。 超出阈值后,会在该代上触发 GC。 因此,分配小型或大型对象时,需要分别使用第 0 代和 LOH 的阈值。 当垃圾回收器分配到第 1 代和第 2 代中时,将使用它们的阈值。 运行此程序时,会动态调整这些阈值。

    这是典型情况,大部分 GC 执行都因为托管堆上的分配。

  • 调用 GC.Collect 方法。

    如果调用无参数 GC.Collect() 方法,或另一个重载作为参数传递到 GC.MaxGeneration,将会一起收集 LOH 和剩余的托管堆。

  • 系统处于内存不足的状况。

    垃圾回收器收到来自操作系统 的高内存通知时,会发生以上情况。 如果垃圾回收器认为执行第 2 代 GC 会有效率,它将触发第 2 代。

1、标记-清除算法
2、压缩算法
3、分代清除算法

标记-清除算法

标记不可达对象,然后将其清除。

标记对象的工作有两种模式:

  • 同步 :标记工作开始之处,就暂停所有线程,开始标记工作。在CLR启动GC之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。目的是防止线程在clr检查期间访问对象并修改其状态。
  • 并发 :起一个低优先级的线程执行标记工作,直到找到有为0的对象,再暂停所有线程,进行垃圾回收工作 在 .NET Core 中,服务器/工作站垃圾回收既可以是非并发也可以是后台执行。

初始化GC,初始化CLR 的同时也初始化了GC。

1、CLR启动时会选择一个GC模式,进程终止前该模式不会改变。
    2、加载 CLR 时,GC 分配两个初始堆段:一个用于小型对象(小型对象堆或 SOH),一个用于大型对象(大型对象堆,每个对象都大于85000字节)。

GC执行阶段(触发垃圾回收后)

1、GC首先挂起所有线程,再判断要回收哪些代,然后应用程序的线程恢复允许。

如果回收第第一代或者第0代那么一切如常进行,如果要回收第二代,就会增加第0代的大小(超过其预算),以便再第0代中分配新对象。然后应用程序的线程恢复允许。

开始一次垃圾回收时候,GC会检查第一代(有过第一代)占用了多少内存,如果第一代远少于预算(内存空间),那么GC只检查第0代的对象。GC 将寻找存在的对象并将它们压缩。 但是由于压缩费用很高,GC 会扫过 LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。 相邻的被清除对象将组成一个自由对象。

2、构建不可达对象。目的是为了找出哪些可以删除,哪些不能删除。

GC运行一个普通的线程优先级的后台线程来查找不可达对象,找到之后再挂起所有线程。

(1)GC标记为“0”阶段。假定全部都可以删除。GC首先假定堆中的所有对象都是不可达unreachable(没有被人应用)的,然后CLR遍历堆中的所有对象,将同步索引字段中的一位设定为0,表示所有对象都因删除

(2)CLR检查所有的活动根,查看它们引用了哪些对象(这就为什么CLR的GC称为引用跟踪GC的原因)如果一个根包含null,CLR忽略这个根并继续检查下个根。

任何根如果引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设为1。一个对象被标记后,CLR会检查那个对象中的根,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了因为循环引用而产生死循环。

标记对象的工作有两种模式:

  • 非并发(同步) :标记工作开始之处,就暂停所有线程,开始标记工作。在CLR启动GC之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。目的是防止线程在clr检查期间访问对象并修改其状态。
  • 后台(并发) :起一个低优先级的线程执行标记和回收工作,直到找到有为0的对象,再暂停所有线程,进行垃圾回收工作 在 .NET Core 中,服务器/工作站垃圾回收既可以是非并发也可以是后台执行。 后台工作方式只影响第 2 代中的垃圾回收;第 0 代和第 1 代中的垃圾回收始终是非并发的,因为它们完成的速度很快。

(3) 在标记完栈中根指向的可达对象后。GC开始标记GC句柄表和GC终结列表中的根指向的可达对象,具体如下:

1、然后垃圾回收器扫描appDomain 的GC句柄表中保护Normal、pinned标记的项,将他们当作根来处理,同时标记执行项目的所引用的对象(已经这些对象的内部字段所引用的对象)。

2、扫描appDomain 的GC句柄表中wesk记录项。如果weak记录项引用了未标记的对象,该引用标识就是不可达对象。该记录项的引用值改为null。

3、GC扫描终结列表,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生。在GC压缩后将复活的对象提升到较老的一代,一个特殊的高优先级CLR线程,执行每个对象的Finale方法,最后清空Freachable队列。下一代垃圾回收时,发现以终结的对象成为真正的垃圾。可终结对象需要执行两次的垃圾回收才能释放它们的占用的内存。

4、weakTrackResurrection不理解  基本用不到

5、GC对内存进行压缩,填补不可达对象留下的内存空洞,这其实就是一个内存碎片整理的过程。Pinned对象不会压缩(移动),垃圾回收器会移动它周围的其他对象。

应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。

可达(reachable)

已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象是可达(reachable)的,因为应用程序代码可通过仍在引用它的变量抵达(或访问)它。

不可达(unreachable)

未标记的对象是不可达(unreachable)的,因为应用程序中不存在使对象能被再次访问的根。

4、GC的压缩(compact)阶段

删除未标记的,然后把可达的对象整到一起形成连续可用的内存空间。整理好对象的内存地址发生变化。不会压缩大对象。AppDomain中的GC句柄表用pinned标记的项也不会移动。

5、修正根的引用。CLR还有将每个根的地址减去整合后偏移的字节数,保证根的地址指向正确内存对象。

6、重置托管堆的NextObjPtr指针。指向最后一个幸存对象之后的位置。

7、CLR恢复所有线程。

如果CLR在一次GC之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明该进程的内存已耗尽。此时,试图分配更多内存的new操作符会抛出OutOMemoryException。应用程序可捕捉该异常并从中恢复。但大多数应用程序都不会这么做:相反,异常会成为未处理异常,Windows将终止进程并回收进程使用的全部内存。

8、汇总这次清除的结果,自动调优。垃圾回收后会检测有各代的多少内存被回收,多少对象幸存,垃圾回收器会根据应用程序的要求的内存负载自由优化。增大或者减小这些代的预算、增加或减少回收的次数。