zoukankan      html  css  js  c++  java
  • C#托管堆和垃圾回收(GC)

    一、基础

    首先,为了深入了解垃圾回收(GC),我们要了解一些基础知识:

    • CLR:Common Language Runtime,即公共语言运行时,是一个可由多种面向CLR的编程语言使用的“运行时”,包括内存管理、程序集加载、安全性、异常处理和线程同步等核心功能。
    • 托管进程中的两种内存堆:
      • 托管堆:CLR维护的用于管理引用类型对象的堆,在进程初始化时,由CLR划出一个地址空间区域作为托管堆。当区域被非垃圾对象填满后,CLR会分配更多的区域,直到整个进程地址空间(受进程的虚拟地址空间限制,32位进程最多分配1.5GB,而64位最多可分配8TB)被填满。
      • 本机堆:由名为VirtualAlloc的Windows API分配的,用于非托管代码所需的内存。
    • NextObjPtr:CLR维护的一个指针,指向下一个对象在堆中的分配位置。初始为地址空间区域的基地址。
    • CLR将对象分为大对象和小对象,两者分配的地址空间区域不同。我们下方的讲解更关注小对象。
      • 大对象:大于等于85000字节的对象。“85000”并非常数,未来可能会更改。
      • 小对象:小于85000字节 的对象。

    然后明确几个前提:

    • CLR要求所有引用类型对象都从托管堆分配。
    • C#是运行于CLR之上的。

    C#new一个新对象时,CLR会执行以下操作:

    1. 计算类型的字段(包括从基类继承的字段)所需的字节数。
    2. 加上对象开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引,32位程序为8字节,64位程序为16字节。
    3. CLR检查托管堆是否有足够的可用空间,如果有,则将对象放入NextObjPtr指向的地址,并将对象分配的字节清零。接着调用构造器,对象引用返回之前,NextObjPtr加上对象真正占用的字节数得到下一个对象的分配位置。

    弄清楚以上知识点后,我们继续来了解CLR是如何进行“垃圾回收”的。

    二、垃圾回收的流程

    我们先来看垃圾回收的算法与主要流程:
    算法:引用跟踪算法。因为只有引用类型的变量才能引用堆上的对象,所以该算法只关心引用类型的变量,我们将所有引用类型的变量称为
    主要流程:
    1.首先,CLR暂停进程中的所有线程。防止线程在CLR检查期间访问对象并更改其状态。
    2.然后,CLR进入GC的标记阶段。
     a. CLR遍历堆中的对象(实际上是某些代的对象,这里可以先认为是所有对象),将同步块索引字段中的一位设为0,表示对象是不可达的,要被删除。
     b. CLR遍历所有,将所引用对象的同步块索引位设为1,表示对象是可达的,要保留。
    3.接着,CLR进入GC的碎片整理阶段。
     a. 将可达对象压缩到连续的内存空间(大对象堆的对象不会被压缩)
     b. 重新计算所引用对象的地址。
    4.最后,NextObjPtr指针指向最后一个可达对象之后的位置,恢复应用程序的所有线程。

    三、垃圾回收的具体细节

    CLR的GC是基于代的垃圾回收器,它假设:

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

    托管堆最多支持三代对象:

    • 第0代对象:新构造的未被GC检查过的对象
    • 第1代对象:被GC检查过1次且保留下来的对象
    • 第2代对象:被GC检查大于等于2次且保留下来的对象

    第0代回收只会回收第0代对象,第1代回收则会回收第0代和第1代对象,而第2代回收表示完全回收,会回收所有对象。

    CLR初始化时,会为第0代和第1代对象选择一个预算容量(单位:KB)。如下图,CLR为ABCD四个第0代对象分配了空间,如果创建一个新的对象导致第0代容量超过预算时,CLR会进行GC。

    A0 B0 C0(不可达) D0       

    GC后的堆如下图,ABD三个对象提升为第1代对象,此时无第0代对象

    A1 B1 D1               

    假设程序继续执行到某个时刻时,托管堆如下,其中FGHIJ为第0代对象

    A1 B1 D1(不可达) F0 G0(不可达) H0 I0 J0

    根据GC假设的前两条可知,它会优先检查第0代对象,那么GC第0代回收后的托管堆如下,FHIJ提升为第1代对象

    A1 B1 D1(不可达) F1 H1 I1 J1       

    随着第1代的增加,GC会发现其占用了太多内存,所以会同时检查第0代和第1代对象,如某个时刻的托管堆如下,其中K为第0代对象

    A1 B1 D1(不可达) F1 H1(不可达) I1 J1 K0

    GC第1代回收后的托管堆如下,其中ABFIJ都为第2代对象,K为第1代对象。

    A2 B2 F2 I2 J2 K1                 

    还有一些额外的规则需要注意:

    • 在进行第1代回收之前,一般都已经对第0代对象回收了好几次了。
    • 如果对象提升到了第2代,它会长期保持存活,基本上只有当GC进行完全垃圾回收(包括0、1、2代的对象)时才会进行回收。
    • 如果GC回收第0代时发现回收了大量内存,则会缩减第0代的预算,这意味着GC更频繁,但做的事情也减少了;反之,如果发现没有多少内存被回收,就会增大第0代的预算,这意味着GC次数更少,但每次回收的内存相对要多。对于第1代和第2代对象来说,也是如此。
    • 如果回收后发现仍然没有得到足够的内存且无法增大预算,GC就会执行一次完全垃圾回收,如果还不够,就会抛出OutOfMemoryException异常。

    四、何时进行垃圾回收

    • 应用程序new一个对象时,CLR发现没有足够的第0代对象预算来分配该对象时
    • 代码显式调用System.GC.Collect()方法时。注意不要滥用该方法
    • Windows报告低内存情况时
    • CLR正在卸载AppDomain时。会回收该AppDomain的所有代对象
    • CLR正在关闭时。CLR在进程正常终止(而不是通过任务管理器等外部终止)时关闭,会回收进程中的所有对象。

    五、垃圾回收模式

    CLR启动时,会选择一个GC主模式,该模式不会更改,直到进程终止。

    • 工作站:默认的,针对客户端应用程序进行优化。GC造成的时延很低,不会导致UI线程出现明显的假死状态
    • 服务器:针对服务器端应用程序进行优化,主要是优化吞吐量和资源利用。

    可以在配置文件中告诉CLR使用服务器回收模式:

    <configuration>
        <runtime>
            <gcServer enabled="true"/>
        </runtime>
    </configuration>
    

    另外,GC还支持两种子模式:并发(默认)和非并发。主要区别在于并发模式中GC有一个额外的后台线程,它能在应用程序运行时并发标记对象。可以在配置文件中告诉CLR不要使用并发回收模式:

    <configuration>
        <runtime>
            <gcConcurrent enabled="false"/>
        </runtime>
    </configuration>
    

    当然,你也可以通过GCSetting类的GCLatencyMode属性对垃圾回收进行某些控制(在你没有完全了解影响的情况下,强烈建议不要更改):

    模式 说明
    Batch 关闭并发GC,.net framework 版本服务器模式默认值
    Interactive 打开并发GC,工作站模式与 .net core 版本服务器模式的默认值
    LowLatency 在短期的、时间敏感的操作中(如动画绘制)使用这个低延迟模式,该模式会尽力阻止第2代垃圾回收,因为花费时间较多,只有当内存过低时才会回收第2代。
    SustainedLowLatency 这个低延迟模式不会导致长时间的GC暂停,该模式会尽力阻止非并发GC线程对第2代垃圾回收(但是允许后台GC线程对其的回收),只有当内存过低时才会阻塞回收第2代,适用于需要迅速响应的应用程序(如股票等)。

    另外,还有一个模式叫做NoGCRegion,用于在程序执行关键路径时将GC线程挂起。但是你不能将该值直接赋值给GCLatencyMode属性,要通过调用System.GC.TryStartGCRegion方法才可以,并调用System.GC.EndGCRegion方法结束。

    六、注意事项

    • 静态字段引用的对象会一直存在,直到用于加载类型的AppDomain卸载为止
    • 由于碎片整理的开销相对较大,因此GC在划算时才会进行碎片整理,并非每次都会执行。
    • 大对象始终为第2代,而且目前版本GC不会压缩大对象,因为移动代价过高。
    • 第0代和第1代总是位于同一个内存段,而第2代可能跨越多个内存段。

    七、特殊的Finalize(终结器)

    包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会造成本机资源的泄漏,为了处理这种情况,CLR提供了称为终结的机制——允许对象在判定为垃圾之后,但在对象内存被回收前执行一些代码。在C#中的表示如下:

    class SomeType
    {
        // 这是一个 Finalize 方法
        ~SomeType() { }
    }
    

    其生成的IL代码为:

    可以看到,C#编译器实际是在模块的元数据中生成了名为Finalizeprotected override方法,并且方法主体的代码被放置在try块中,并在finally块中调用base.Finalize(本例调用了Object的终结器)。

    那么,终结的内部是如何工作的呢?

    1. new新对象时,如果该对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表中,该列表由GC内部控制。
    2. 当可终结对象被回收时,会将引用从终结列表移动到freachable队列中,该队列由GC内部控制。
    3. CLR会启用一个特殊的高优先级线程来专门调用Finalze方法。freachable队列为空时,该线程将睡眠;但一旦队列中有记录项出现,线程就会被唤醒,将每一项都从freachable队列中移除,并调用每个对象的Finalize方法。

    如果类型的Finalize方法是从System.Object继承的,CLR就不认为该对象是“可终结”的,只有当类型重写了ObjectFinalize方法时,才会将类型及其派生类型的对象视为“可终结”的。

    注意,除非有必要,否则应尽量避免定义终结器。原因如下:

    • 可终结对象在回收时,必须保证存活,这就可能导致其被提升为另一代,生存期延长,导致内存无法及时回收。另外,其内部引用的所有对象也必须保证都存活,一些被认为是垃圾的对象在可终结对象回收后也无法直接回收,直到下一次(甚至多次)GC时才会被回收。
    • Finalize 方法在GC完成后才会执行,而GC的执行时机无法控制,也就导致该方法的执行时间也无法控制。
    • Finalize 方法中不要访问其他可终结对象,因为CLR无法保证多个 Finalize 方法的执行顺序。如果访问了已终结的对象,Finalize 方法抛出未处理的异常,导致进程终止,无法捕捉异常。

    在实际项目开发中,想要避免释放本机资源基本不可能,但是我们可以通过规范代码来规避异常,这就需要用到IDisposable接口了。示例代码如下:

    public class MyResourceHog : IDisposable
    {
        //标识资源是否已被释放
        private bool _hasDisposed = false;
    
        public void Dispose()
        {
            Dispose(true);
            //阻止GC调用 Finalize
            GC.SuppressFinalize(this);
        }
    
        /// <summary>
        /// 如果类本身包含非托管资源,才需要实现 Finalize
        /// </summary>
        ~MyResourceHog()
        {
            Dispose(false);
        }
    
        protected virtual void Dispose(bool isDisposing)
        {
            if (_hasDisposed) return;
    
            //表明由 Dispose 调用
            if (isDisposing)
            {
                //释放托管资源
            }
            //释放非托管资源。无论 Dispose 还是 Finalize 调用,都应该释放非托管资源
    
            _hasDisposed = true;
        }
    }
    
    public class DerivedResourceHog : MyResourceHog
    {
        //基类与继承类应该使用各自的标识,防止子类设置为true时无法执行基类
        private bool _hasDisposed = false;
    
        protected override void Dispose(bool isDisposing)
        {
            if (_hasDisposed) return;
    
            if (isDisposing)
            {
                //释放托管资源
            }
            //释放非托管资源
    
            base.Dispose(isDisposing);
    
            _hasDisposed = true;
        }
    }
    
  • 相关阅读:
    如何:为 Silverlight 客户端生成双工服务
    Microsoft Sync Framework 2.1 软件开发包 (SDK)
    Windows 下的安装phpMoAdmin
    asp.net安全检测工具 Padding Oracle 检测
    HTTP Basic Authentication for RESTFul Service
    Windows系统性能分析
    Windows Server AppFabric Management Pack for Operations Manager 2007
    Mongo Database 性能优化
    服务器未能识别 HTTP 标头 SOAPAction 的值
    TCP WAIT状态及其对繁忙的服务器的影响
  • 原文地址:https://www.cnblogs.com/xiaoxiaotank/p/11193745.html
Copyright © 2011-2022 走看看