【C#托管堆和垃圾回收】开篇


参考:microsoft垃圾回收的基本知识、CLR via C# 托管堆和垃圾回收

托管堆是什么?.

托管堆:CLR要求所有对象都从托管堆中分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。

(CLR还要维护一个指针,我们称它作NextObjPtr。该指针指向下一个对象在堆中的分配位置)

托管堆基础,托管堆分配资源

在面向对象中,每个类型都代表可供程序提供的一种资源,要使用这些资源,必须为代表资源的这些类型分配内存
1.调用IL指令newobj,为代表资源的类型分配内存(一般使用C#new操作符来完成)
2.访问类型的成员来使用资源(有必要可以重复)
3.摧毁资源的状态以进行清理
4.释放内存。垃圾回收器独自负责这一步

C#的new操作符导致CLR执行以下步骤

1.计算类型的字段(以及从基类型继承的字段)所需的字节数
2.加上对象开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。
   32位应用程序,这两个字段各自需要32位,所以每个对象要增加8字节
   64位应用程序,这两个字段各自需要64位,所以每个对象要增加16字节
3.CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的可用空间,就在NextObjPtr指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操作符返回对象引用。就在返回这个引用之前,NextObjPtr指针的值会加上对象占用的字节数来得到一个新值,及下个对象放入托管堆时的地址

触发垃圾回收的因素

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

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

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

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

  • 应用程序调用new操作符创建对象,发现没有足够的地址空间来分配对象,CLR就经行垃圾回收。
  • CLR卸载APPDomain,所有代0、1、2的垃圾回收。
  • CLR正在关闭,收回内存

垃圾回收算法

1、开始一次垃圾回收时候,GC会检查第一代(有过第一代)占用了多少内存,如果第一代远少于预算(内存空间),那么GC只检查第0代占用了多少对象。

2、GC标记为“0”阶段

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

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

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

3、构建不可达对象。目的是为了找出哪些可以删除,哪些不能删除。CLR检查所有的活动根,查看它们引用了哪些对象(这就为什么CLR的GC称为引用跟踪GC的原因)如果一个根包含null,CLR忽略这个根并继续检查下个根。

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

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

可达(reachable)

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

不可达(unreachable)

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

4、GC的压缩(compact)阶段

删除未标记的,然后把可达的对象整到一起形成连续可用的内存空间。整理好对象的内存地址发生变化。

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

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

7、CLR恢复所有线程。

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

提升性能

CLR的GC是基于代的垃圾回收器(generational garbage collector),它对你的代码做出了以下几种假设。

  • 对象越新,生存期越短
  • 对象越老,生存期越大
  • 回收堆的一部分,速度快于整个堆

第2代的产生的过程:GC对第0代对象执行一个完整的GC算法后产生第一代。对新产生的的第0代对象执行算法又产生第一代,将新产生的第一代和原先的第一代放在一起。如此反复多次后,将产生大量的第一代对象,直到用完了第一代的预算内存。

新一轮垃圾回收时候,GC将检查第0代和第1代的所有对象,此次垃圾回收后将产生第1代和第2代幸存对象。托管堆只支持0、1、2代回收。

完整 的过程请看CLR第4版P454- P456。

注释:

 (1)CLR初始化时,会为每一代选择预算。然而,CLR的垃圾回收器是自调节的。这意味着垃圾回收器会在执行垃圾回收的过程中了解应用程序的行为。

(2)如果垃圾回收器发现在回收0代后存活下来的对象很少,就可能减少第0代的预算。已分配空间的减少意味着垃圾回收将更频繁的发生。

(3)另一个方面,如果垃圾回收器收了第0代,发现还有很多对象的话,没有多少内存被回收就会增大第0代的预算。现在,垃圾回收的次数将减少,但每次进行垃圾回收时,回收的内存要多得多。如果没有回收到足够的内存,垃圾回收器会执行一次完整的回收

如果还是不够,就抛出OutOfMemoryException异常。

大对象和小对象

CLR将对象分为大对象和小对象。本章到目前为止说的都是小对象。目前认为85000字节或更大的对象是大对象。

CLR以不同方式对待大小对象:

   

  • CLR检测第0代超过预算时候触发一次GC
  • 大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配。
  • 目前版本的GC不压缩大对象,因为在内存中移动它们代价过高。但这可能在进程中的大对象之间造成地址空间的碎片化,以至于抛出OutOMemoryException.CLR将来的版本可能压缩大对象。
  • 大对象总是第2代,绝不可能是第0代或第1代。所以只能为需要长时间存活的资源创建大对象。分配短时间存活的大对象会导致第2代被更频繁地回收,会损害性能。大对象一般是大字符串(比如XML或JSON)或者用于10操作的字节数组(比如从文件或网络将字节读入缓冲区以便处理)。

工作站和服务器垃圾回收

使用条件:进程终止前不会改变,不过可用通过GCsetting类的GClatencyMode进行控制。

工作站
该模式针对客户端应用程序优化GC.GC造成的延时很低,应用程序线程挂起时间很短,避免使用户感到焦虑。在该模式中,GC假定机器上运行的其他应用程序都不会消耗太多的CPU资源。

工作站垃圾回收既可以是并发的,也可以是非并发的。 并发(或后台 )垃圾回收使托管线程能够在垃圾回收期间继续操作。 后台垃圾回收替换 .NET Framework 4 及更高版本中的并行垃圾回收。
服务器
该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。GC假定机器上没有运行其他应用程序(无论客户端还是服务器应用程序),并假定机器的所有CPU都可用来辅助完成GC。该模式造成托管堆被拆分成几个区域(section),每个CPU一个。开始垃圾回收时,垃圾回收器在每个CPU上都运行一个特殊线程;每个线程都和其他线程并发回收它自己的区域。对于工作者线程(worker thread)行为一致的服务器应用程序,并发回收能很好地进行。这个功能要求应用程序在多CPU计算机上运行,使线程能真正地同时工作,从而获得性能的提升。

  • 在 .NET Core 中,服务器垃圾回收既可以是非并发也可以是后台执行。

  • 在 .NET Framework 4.5 和更高版本中,服务器垃圾回收既可以是非并发也可以是后台执行。 在 .NET Framework 4 和以前的版本中,服务器垃圾回收非并行运行

应用程序默认以“工作站”GC模式运行。寄宿了CLR的服务器应用程序(比如ASP.NET或Microsoft SQL Server)可请求CLR加载“服务器”GC.但如果服务器应用程序在单处理器计算机上运行,CLR将总是使用“工作站"GC模式。

独立应用程序可创建一个配置文件告诉CLR使用服务器回收器。配置文件要为应用程序添加一个gcServer元素。下面是一个示例配置文件:

   
       
        "true"/>    
        

应用程序运行时,可查询GCSettings类的只读Boolean属性IsServerGC来询问CLR它是否正在“服务器”GC模式中运行:

using System;  
using System.Runtime; // GCSettings is in this namespace  
  
public static class Program {  
   public static void Main() {   
      Console.WriteLine("Application is running with server GC=" + GCSettings.IsServerGC);   
    }   
}

除了这两种主要模式,GC还支持两种子模式:并发(默认)或非并发。

在并发方式中,垃圾回收器有一个额外的后台线程,它能在应用程序运行时并发标记对象。一个线程因为分配对象造成第0代超出预算时,GC首先挂起所有线程,再判断要回收哪些代。
事实上,垃圾回收器更倾向于选择不压缩。可用内存多,垃圾回收器便不会压缩堆;这有利于增强性能,但会增大应用程序的工作集。

为了告诉CLR不要使用并发回收器,可创建包含geConcurrent元素的应用程序配置文件。下面是配置文件的一个例子:

   
      
      "false"/>   
     

延迟模式 lowlatency

使用环境:GC模式是针对进程配置的,进程运行期间不能更改,但是应用程序使用GCsetting类的GClatencyMode属性对垃圾回收进行某种程度的控制。

若要回收对象,垃圾回收器 (GC) 必须停止应用程序中所有正在执行的线程。 垃圾回收器处于活动状态的时间段称为延迟 。

在某些情况下(例如当应用程序检索数据或显示内容时),关键时刻可能发生完整的垃圾回收,从而妨碍性能。 可以通过将 GCSettings.LatencyMode 属性设置为其中一个 System.Runtime.GCLatencyMode 值来调节垃圾回收的干扰。

低延迟设置

使用“低”延迟设置意味着垃圾回收器对应用程序的干扰较少。 垃圾回收在回收内存方面较为保守。

System.Runtime.GCLatencyMode 枚举提供两种低延迟设置:

  • GCLatencyMode.LowLatency 禁止第 2 代回收,仅执行第 0 代和第 1 代回收。 只能在短时间内使用。 在更长时间内,如果系统处于内存压力下,垃圾回收器将触发一次回收,这样会暂时暂停应用程序并中断对时间要求很急的操作。 此设置仅对工作站垃圾回收可用。

  • GCLatencyMode.SustainedLowLatency 禁止第 2 代前台回收,仅执行第 0 代、第 1 代回收和第 2 代后台回收。 它可以长时间使用,并对工作站和服务器垃圾回收都可用。 如果后台垃圾回收已禁用,则无法使用此设置。

在低延迟期间,除非发生以下情况,否则禁止第 2 代回收:

  • 系统收到操作系统的低内存通知。

  • 应用程序代码通过调用 GC.Collect 方法并将 generation 参数指定为 2 来包含回收。

方案

下表列出了使用 GCLatencyMode 值的应用程序方案:

方案
延迟模式应用程序方案
Batch 对于不具有用户界面 (UI) 或服务器端操作的应用程序。

禁用后台垃圾回收后,这将是服务器垃圾回收的默认模式。 Batch 模式还会替代 gcConcurrent 设置,即它会阻止后台或并发回收。
Interactive 对于具有 UI 的大多数应用程序。

这是工作站垃圾回收的默认模式。 但是,如果托管了某个应用,则优先考虑托管进程的垃圾回收器设置。
LowLatency 对于具有短期时效性操作(操作期间垃圾回收器的干扰可能会引起中断)的应用程序。 例如,呈现动画或数据采集功能的应用程序。
SustainedLowLatency 适用于在有限但有可能更长的时间内具有时效性操作并且在此期间垃圾回收器中断具有破环性的应用程序。 例如,需要随着交易时间内的市场数据变化做出快速响应的应用程序。

此模式会比其他模式产生更大的托管堆大小。 由于它不压缩托管堆,因此可能产生更多碎片。 确保有足够的可用内存。

低延迟使用指南

使用 GCLatencyMode.LowLatency 模式时,请注意以下指导原则:

  • 尽可能地缩短低延迟时段。

  • 避免在低延迟时段分配大量内存。 由于垃圾回收回收的对象较少可能出现低内存通知。

  • 在低延迟模式下,最大限度减少新的分配次数,尤其是分配到大型对象堆和固定对象的次数。

  • 知道可以分配的线程。 由于 LatencyMode 属性设置属于进程范围的设置,因此可以在分配的任何线程上生成 OutOfMemoryException 异常。

  • 将低延迟代码包装在受约束的执行区域中。 有关详细信息,请参阅受约束的执行区域。

  • 在低延迟期间,可以通过调用 GC.Collect(Int32, GCCollectionMode) 方法强制进行第 2 代回收。

  • 假如有 多个线程要操作这个设置,考虑用interlocked

     

LatencyMode使用案例

private static void LowLatencyDemo() {
 GCLatencyMode oldMode = GCSettings.LatencyMode;
 System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions(); //CER 约束执行区
 try {
 GCSettings.LatencyMode = GCLatencyMode.LowLatency;
 // Run your code here...
 }
 finally {
 GCSettings.LatencyMode = oldMode;
 }
}
C