zoukankan      html  css  js  c++  java
  • 托管堆和垃圾回收

    一、托管堆基础

    1,访问一个资源(文件、内存缓冲区、屏幕空间、网络连接、数据库资源等)所需的步骤

    ①调用IL指令newobj,为代表资源的类型分配内存(一般使用c# new操作符来完成)

    ②初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态

    ③访问类型的成员来使用资源(有必要可以重复)

    ④摧毁资源的状态以进行清理

    ⑤释放内存。垃圾回收器独自负责这一步

    2,从托管堆分配资源

    初始化进程时,CLR划出一个地址空间区域作为托管堆,一个区域被非垃圾对象填满后,CLR会分配更多的区域(32位进程最多能分配1.5GB,64为进程最多能分配8TB)。CLR还要维护一个指针(NextObjPtr),该指针指向下一个对象在堆中的分配位值。刚开始的时候,NextObjPtr设为地址空间区域的基地址。

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

    ①计算类型的字段(以及从基类型继承的字段)所需的字节数。

    ②加上对象的开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步快索引(32位:两个字段各需32位,所以每个对象要增加8字节。64位:每个字段各需64位,所以每个对象要增加16字节)(int=4字节;long=8字节)

    ③CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的可用空间,就在NextObjPtr指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操作符返回对象的引用。就在返回这个引用之前,NextObjPtr指针的值会加上对象占用的字节数来得到一个新值,即下一个对象放入托管堆是的地址

    4,垃圾回收算法

    CLR使用一种引用跟踪算法。引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象,我们将所有引用类型的变量都称为

    ①CLR开始GC时,首先暂停进程中的所有线程(这样可以防止线程在CLR检查期间访问对象并更改其状态)

    ②CLR进入GC标记阶段(这个阶段,CLR遍历堆中所有对象,将同步块索引字段中的一位设为0。这表明所有的对象都应该删除)

    ③CLR检查所有活动根(根为null,则CLR忽略这个根),查看他们引用了那些对象。如果引用了堆上的对象,CLR都会标记那个对象(将对象的同步块索引中的位设置为1)

    ④检查完毕后,堆中的对象要么标记。要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它,我们说这些对象时可达

    ⑤进入GC的压缩阶段,在这个阶段,CLR对堆中已标记的对象进行“乾坤大挪移”,压缩所有幸存下来的对象,使它们占用连续的内存对象

    ⑥压缩之后,根现在的引用还是原来的位置,而非移动之后的位置。所以作为压缩阶段的一部分,CLR还要从每个根减去所引用的对象在内存中的偏移的字节数。这样就能保证根还是引用和之前一样的对象;只是对象在内存中换了位置

     

    5,垃圾回收和调试

    ①使用Release编译后,允许可执行文件,会发现TimerCallback方法只被调用了一次。因为Timer在初始化之后再也没有用过变量t。(调试模式下Timer对象不会被回收)

            static void Main(string[] args)
            {
                //创建没2000毫秒就调用一次TimerCallback方法的timer对象
                Timer t = new Timer(TimerCallback, null, 0, 2000);
                Console.ReadLine();
            }
            private static void TimerCallback(object o)
            {
                Console.WriteLine("a");
    
                //出于演示目的,强制执行一次垃圾回收
                GC.Collect();
            }

    ②显示要求释放计时器,它才能活到被释放的那一刻

            static void Main(string[] args)
            {
                //创建没2000毫秒就调用一次TimerCallback方法的timer对象
                Timer t = new Timer(TimerCallback, null, 0, 2000);
                Console.ReadLine();
                //在ReadLine之后引用t(在Dispose方法返回之前,t会在GC中存活)
                t.Dispose();
            }
            private static void TimerCallback(object o)
            {
                Console.WriteLine("a");
    
                //出于演示目的,强制执行一次垃圾回收
                GC.Collect();
            }

    二、代:提升性能

    对象越新,生存期越短
    对象越老,生存期越长
    回收堆的一部分,速度快于回收整个堆
    1,原理
    ①CLR初始化堆时为0代和1代选择预算容量(以kb为单位)。后期CLR会自动调节预算容量
    ②如果分配一个新的对象造成第0代超过预算,就必须启动一次垃圾回收
    ③经过垃圾回收之后,第0代的幸存者被提升到1代(第一代的大小增加);第0代又空了出来
    ④由于第0代已满,所以必须垃圾回收。但这一次垃圾回收器发现第1代用完了预算容量。所以这次垃圾回收器决定检查第1代和第0代的所有对象。两代被垃圾回收以后,第1代的幸存者提升到2代,第0代的幸存者提升到1代

    2,垃圾回收触发的条件
    ①最常见触发条件:CLR在检查第0代超过预算时触发一次GC
    ②代码显示调用Sytem.GC的静态Collect方法
    ③Windows报告底内存情况
    ④CLR正在卸载AppDomain
    ⑤CLR正在关闭(CLR在进程正常终止时)

    3,大对象
    目前认为85000字节或更大的对象时大对象。(之前讨论的都是小对象)。大对象一般是大字符串(比如XML或JSON)或者用于I/O操作的字节数组(比如从文件或网络将字节读入缓冲区一遍处理)
    ①大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配
    ②目前版本的GC不压缩大对象,因为在内存中移动它们的代价过高
    ③大对象总是第2代,绝不可能是第0代或者第1代

    4,垃圾回收模式

    CLR启动时会选择一个GC模式,进程终止前该模式不会变。

    ①两个主要模式:

    1>工作站

    该模式针对客户端应用程序优化GC。GC造成的延时很低,应用程序线程挂起时间很短,避免是用户感到焦虑。

    2>服务器

    该模式针对服务器应用程序优化GC。被优化的主要是吞吐量和资源利用。

    ②应用程序模式以“工作站”GC模式运行

    ③显示告诉CLR使用服务器回收站

      <runtime>
        <gcServer enabled="true"></gcServer>
      </runtime>
                //询问CLR它是否在“服务器”GC模式中运行
                Console.WriteLine(GCSettings.IsServerGC);
                Console.ReadLine();

     ④两个子模式(并发(默认)或非并发)

    在并发模式中,垃圾回收器有一个额外的后台线性,它能在应用程序运行时并发标记对象

      <runtime>
        <!--告诉CLR不要使用并发回收器-->
        <gcConcurrent enabled="false"></gcConcurrent>
      </runtime>

     ⑤GCSettings的LatencyMode属性对垃圾回收进行某种程度的控制

    符号名称

    说明

    Batch(“服务器”GC模式的默认值)

    关闭并发GC

    Interactive(“工作站”GC模式的默认值)

    打开并发GC

    LowLatency

    在短期的、时间敏感的操作中(如果动画绘制)使用这个延迟模式。这些操作不适合对第二代进行回收

    Sustained LowLatency

    使用这个延迟模式,应用程序的大多数操作都不会发生长的GC暂停。只要有足够的内存,它将禁止所有会造成阻塞的第二代回收操作。事实上,这种应用程序(例如需要迅速响应的股票软件)的用户应该考虑安装更多的RAM来防止发生生长的GC暂停

    ⑥正确的使用LowLatency

            static void Main(string[] args)
            {
    
                GCLatencyMode oldModel = GCSettings.LatencyMode;
                Console.WriteLine(oldModel);
                
                //约束执行区域(CER)
                System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
                try
                {
                    GCSettings.LatencyMode = GCLatencyMode.LowLatency;
                    //在这里运行你的代码...
                }
                finally
                {
                    GCSettings.LatencyMode = oldModel;
                }
                Console.ReadLine();
    
            }
    View Code

     5,强制垃圾回收

    public static void Collect(int generation, System.GCCollectionMode mode, bool blocking, bool compacting)

    符号名称

    说明

    Default

    等同于不传递任何符号名称。目前还等同于Forced,但未来的版本可能对此进行修改

    Forced

    强制回收指定的代(以及低于它的所有代)

    Optimized

    只有在能释放大量内存或者能减少碎片化的前提下,才执行回收。如果垃圾回收没有什么效率,当前调用就没有任何效果

    如果写一个CUI(控制台用户界面)或GUI(图形用户界面)应用程序,你可能希望建议垃圾回收的时间;为此,请将GCCollectionMode设置为Optimized并调用Collect。Default和Forced模式一般用于调试、测试和查找内存泄露

    如果刚才发生了某个非重复性的事件,并导致大量旧对象死亡,就可考虑手动调用一次collect方法。由于是非重复性事件,垃圾回收器基于历史的预测可能不准确。所以,这是调用collect方法时合适的

                //查看某一代发生了多少次垃圾回收
                Console.WriteLine(GC.CollectionCount(0));
                //查看托管堆中的对象当前使用了多少内存
                Console.WriteLine(GC.GetTotalMemory(true));

    三、使用需要特殊清理的类型

    包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会造成本机资源的泄漏,所以CLR提供了称为终结的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源(文件、网络连接、套接字、互斥体)的类型都支持终结。CLR判定一个对象不可达时,对象将终结自己,释放它包装的本机资源。之后,GC会从托管堆回收对象

    1,Finalize

    它是为释放本机资源而设计的

        internal sealed class SomeType
        {
            //这是一个Finalize方法
            ~SomeType()
            {
                //这里的代码会进入Finalize方法
            }
        }

     2,SafeHandle

    创建封装了本机资源的托管类型是,应该先从using System.Runtime.InteropServices.SafeHandle这个特殊基类派生一个类

        public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
        {
            //这是本机资源句柄
            protected IntPtr handle;
    
            protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle)
            {
                handle = invalidHandleValue;
                //如果ownsHandle为true,那么这个从SafeHandle派生的对象被回收时,本机资源会被关闭
            }
    
            protected SafeHandle(IntPtr invalidHandleValue)
            {
                handle = invalidHandleValue;
            }
    
            //显式释放资源
            public void Dispose(){Dispose(true);}
    
            //默认的Dispose实现(如下所示)正是我们希望的。强烈建议不要重写这个方法
            protected virtual void Dispose(Boolean disposing)
            {
                //这个默认实现会忽略disposing参数
                //如果资源已经释放,那么返回
                //如果ownsHandle为true,那么返回
                //设置一个标志来指明该资源已经释放
                //调用虚方法ReleaseHandle
                //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法
                //如果ReleaseHandled返回true,那么返回
                //如果走到这一步,就激活ReleaseHandleFailed托管调试助手(MDA)
    
            }
            //派生类型要从写这个方法以实现释放资源的代码
            protected abstract Boolean ReleaseHandle();
    
            //默认的Dispose实现(如下所示)正是我们希望的。强烈建议不要重写这个方法
            ~SafeHandle(){Dispose(false);}
    
            public void SetHandleAsInvalid()
            {
                //设置标志来指出这个资源已经释放
                //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法
            }
    
            public Boolean IsClosed {
                get { //返回指出资源是否释放的一个标志}
            }
            public abstract Boolean IsInvalid
            {
                //派生类要重写这个属性
                //如果句柄的值不代表资源(通常意味着句柄为0或-1),实现应返回true
                get;
            }
    
            //以下三个方法设计安全性和引用计数
            public void DangerousAddRef(ref Boolean success){}
            public IntPtr DangerousGetHandle(){}
            public void DangerousRelease(){}
    
        }

    CLR赋予这个类以下三个很酷的功能

    ①首次构造CriticalFinalizerObject派生类型对象时,CLR立即对继承层次结构中的所有Finalize方法进行JIT编译。构造对象时接编译这些方法,可确保放当对象被确定为垃圾之后,本机资源肯定会得以释放。不对Finalize方法进行提前编译,那么也许能分配并使用本机资源,但无法保证释放。内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这会阻止Finalize方法的执行,造成本机资源泄漏。另外,如果Finalize方法中的代码引用了另一个程序集中的类型,但CLR定位该程序集失败,那么资源将得不到释放。

    ②CLR是在调用了非CriticalFinalizerObject派生类型的Finalize方法之后,才调用CriticalFinalizerObject派生类的Finalize方法。这样,托管资源类就可以在它们的Finalize方法中成功地访问CriticalFinalizerObject派生类型的对象。例如,FileStram类型的Finalize方法可以放心地将数据从内存缓冲区flush到磁盘,它知道此时磁盘文件还没有关闭

    ③如果AppDomain被一个宿主应用程序(例如Microsoft SQL Server或者Microsoft ASP.NET)强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序不再信任它内部允许的托管代码,也利用这个功能确保本机资源得以释放。

    3,SafeHandle派生类

    SafeHandle派生类非常有用,因为它们保证本机资源在垃圾回收得以释放

        internal static class SomeType
        {
            //这个原型不健壮
            [DllImport("Kernal32",CharSet = CharSet.Unicode,EntryPoint = "CreateEvent")]
            private static extern IntPtr CreateEventBad(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState,
                string name);
    
            //这个原型是健壮的
            [DllImport("Kernal32", CharSet = CharSet.Unicode, EntryPoint = "CreateEvent")]
            private static  extern SafeWaitHandle CreateEventGood(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState,
                string name);
    
            public static void SomeMethod()
            {
                IntPtr handle = CreateEventBad(IntPtr.Zero, false, false, null);
                SafeWaitHandle swh = CreateEventGood(IntPtr.Zero, false, false, null);
            }
        }
    SomeType

    4,使用包装了本机资源的类型

    1>以FileStream为例,可以用它打开一个文件,从文件中读取字节,向文件中写入字节,然后关闭文件
    ①FileStream对象在构造时会调用Win32 CreateFile函数
    ②函数返回句柄保存到SafeFileHandle对象中
    ③然后通过FileStream对象的一个私有字段来维护对象的引用

    2>FileStream的Dispose方法

    ①FileStream实现了IDisposable接口。FileStream的Dispose方法会调用SafeFileHandle字段上的Dispose方法。
    FileStream调用Dispose方法会清理本机资源。(并非一定要调用Dispose才能保证本机资源得以清理。本机资源的清理最终总会发生,调用Dispose只是控制这个清理动作的发生时间)
    FileStream调用Dispose方法不会导致FileStram对象从托管堆中删除。只有在垃圾回收之后,托管堆中的内存才会得以回收

            static void Main(string[] args)
            {
                //创建要写入临时文件的字节
                byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5};
    
                //创建临时文件
                FileStream fs = new FileStream("Temp.dat", FileMode.Create);
    
                //将字节写入临时文件
                fs.Write(bytesToWrite, 0, bytesToWrite.Length);
    
                //删除临时文件
                File.Delete("Temp.dat");//抛出IOException异常
    
                Console.ReadLine();
            }
            static void Main(string[] args)
            {
                //创建要写入临时文件的字节
                byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5};
    
                //创建临时文件
                FileStream fs = new FileStream("Temp.dat", FileMode.Create);
    
                //将字节写入临时文件
                fs.Write(bytesToWrite, 0, bytesToWrite.Length);
                
                //结束写入后显式关闭文件
                fs.Dispose();
    
                fs.Write(bytesToWrite, 0, bytesToWrite.Length);//抛出ObjectDisposedException
    
                //删除临时文件
                File.Delete("Temp.dat");//抛出IOException异常
    
                Console.ReadLine();
            }

     5,一个有趣的依赖性问题

                //创建临时文件
                FileStream fs = new FileStream("Temp.txt", FileMode.Create);
                StreamWriter sw = new StreamWriter(fs);
                sw.Write("abc");
                //不要忘记这个Dispose的调用,不执行sw.Dispose()数据写不进文件
                sw.Dispose();
                //注意:调用StreamWriter.Dispose会关闭FileStream;
                //FileStream对象无需显示关闭

    不需要再FileStream对象上显式调用Dispose,因为StreanWrite会帮你调用。但如果非要显式调用Dispose,FileStream会发现对象已经清理过了,所以方法什么都不做直接返回

     6,终结器的内部工作原理

    ①应用程序创建新对象时,New操作符会从推中分配内存。如果对象的类型定义了Funalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表

    ②垃圾回收时,对象B,D,E,F判定为垃圾。垃圾回收器扫描终结列表以查找这些对象的引用。找到一个引用之后,该引用从终结列表中移除,并附加到freachable队列中

    ③一个特殊的高优先级CLR线程专门调用Finalize方法。一旦freachable队列中有记录项出现,线程就会唤醒,将每一项都从freachable队列中移除,同时调用每个对象的Finalize方法。

    ④下一次对老一代垃圾回收时,会发现已终结的对象成为真正的垃圾,因为没有应用程序的根指向它们,freachable队列也不再指向它们,所以,这些对象的内存会直接回收

    (注意:CLR会忽略System.Object定义的Finalize方法)

    (注意:可终结对象需要执行两次垃圾回收才能释放它们的内存。在实际应用中,由于对象可能被提升至另一代,所以可能要求不止进行两次垃圾回收)

    7,手动监视和控制对象的生存期

        public struct GCHandle
        {
            //静态方法,用于在表中创建一个记录项
            public static GCHandle Alloc(object value);
            public static GCHandle Alloc(object value,GCHandleType type);
    
            //静态方法,用于将一个GCHandle转成一个IntPtr
            public static explicit operator IntPtr(GCHandle value);
            public static IntPtr ToIntPtr(GCHandle value);
    
            //静态方法,用于将一个IntPtr转成一个GCHandle
            public static explicit operator GCHandle(IntPtr value);
            public static GCHandle FromIntPtr(IntPtr value);
    
            //实例方法,用于释放表中的记录项(索引设置为0)
            public void Free();
    
            //实例属性,用于get/set记录项的对象引用
            public object Target { get; set; }
    
            //实例属性,如果索引不为0,就放回true
            public Boolean IsAllocated { get; }
    
            //对于已固定(pinned)的记录项,这个方法返回对象的地址
            public IntPtr AddrOfPinnedObject();
        }
        public enum GCHandleType
        {
            Weak = 0, //监事对象的存在
            WeakTrackResurrection = 1, //监事对象的存在
            Normal = 2, //控制对象的生存期
            Pinned = 3 //控制对象的生存期
        }

    Weak:

    该标志允许监视对象的生存期。可检测垃圾回收器再什么时候判定该对象在应用程序代码中不可达。注意,此时对象的Finalize方法可能执行,也可能没有执行,对象可能还在内存中。

    WeakTrackResurrection:

    该标志允许监视对象的生存期。可检测垃圾回收器在什么时候判定该对象在应用程序的代码不可达。注意,此时对象的Finalize方法(如果有的话)已经执行,对象的内存已经回收

    Normal:

    该标志允许控制对象的生存期。告诉垃圾回收器:即使应用程序中没有根引用对象,该对象也必须留在内存中。垃圾回收发生时,该对象的内存可以压缩(移动)。Alloc方法默认的标志

    Pinned: 

    该标志允许控制对象的生存期。告诉垃圾回收器:即使应用程序中没有根引用对象,该对象也必须留在内存中。垃圾回收发生时,该对象的内存不压缩(移动)。需要将内存地址交给本机代码时,这个功能很好用。本机代码知道GC不会移动对象,所以能放心地向托管堆的这个内存写入。

    1>垃圾回收器如何使用GC句柄表。当垃圾回收发生时,垃圾回收器的行为如下
    ①垃圾回收器标记所有可达的对象。然后。垃圾回收器扫描GC句柄表;所有Normal或Pinned对象都被看成是根,同时标记这些对象(包括对象通过他们的字段引用的对象)
    ②垃圾回收器扫描GC句柄表,查找所有Weak记录项。如果一个Weak记录项引用了未标记的对象,该引用标识的就是不可达对象(垃圾),记录项的引用值更改为null
    ③垃圾回收器扫描终结列表。在列表中,对未标记对象的引用标识的是不可达对象,这个引用从终结列表移至freachable队列,这是对象会被标记,因为对象又变成可达了
    ④垃圾回收器扫描GC句柄表,查找所有WeakTrackResurrection记录项。如果一个WeakTrackResurrection记录项引用了未标记的对象(它现在是有freachable队列中的记录项引用的),该引用标识的就是不可达对象(垃圾),该记录项的引用值更改为null
    ⑤垃圾回收器对内存进行压缩,填补不可达对象留下的内存“空调”,这其实就是一个内存碎片整理的过程。Pinned对象不会压缩(移动),垃圾回收器会移动它周围的其他对象

    学习永不止境,技术成就梦想。
  • 相关阅读:
    人工智能数学基础笔记(上)
    人工智能简介
    十三,十四 基金收益,税收与基金国际化
    资产配置模型之-BL模型
    十二 基金估值,费用与会计核算
    十一 基金的投资交易与结算
    十 基金业绩评价
    九 投资风险管理
    浙工商oj ___飞龙的飞行方程
    hd1004解题思路
  • 原文地址:https://www.cnblogs.com/zd1994/p/7102737.html
Copyright © 2011-2022 走看看