托管堆和垃圾回收

阅读: 评论:0

托管堆和垃圾回收

托管堆和垃圾回收

一、托管堆基础

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),实现应返回trueget;}//以下三个方法设计安全性和引用计数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(&#", 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转成一个IntPtrpublic static explicit operator IntPtr(GCHandle value);public static IntPtr ToIntPtr(GCHandle value);//静态方法,用于将一个IntPtr转成一个GCHandlepublic static explicit operator GCHandle(IntPtr value);public static GCHandle FromIntPtr(IntPtr value);//实例方法,用于释放表中的记录项(索引设置为0)public void Free();//实例属性,用于get/set记录项的对象引用public object Target { get; set; }//实例属性,如果索引不为0,就放回truepublic 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对象不会压缩(移动),垃圾回收器会移动它周围的其他对象

 

转载于:.html

本文发布于:2024-01-29 14:19:11,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170650915415883.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:垃圾
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23