第二十一章 托管堆和垃圾回收

目录:

21.1 托管堆基础

21.2 代:提升性能

21.3 使用需要特殊清理的类型

21.1 托管堆基础

在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存,访问一个资源所需步骤:

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

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

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

4.推毁资源的状态以进行清理。

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

1.1 从托管堆分配资源

CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。CLR还要维护一个指针(NextObjPtr),该指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr设为地址空间区域的基地址。

一个区域被非垃圾对象填满后,CLR会分配更多的区域。这个过程一直重复,直至整个进程地址空间都被填满。(所以应用程序的内存受进程的虚拟地址空间的限制)

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

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

2.加上对象的开销所需要的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。

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

对于托管堆,分配对象只需在指针上加一个值-速度相当快。

1.2 垃圾回收算法

没有足够地址空间来分配对象时,CLR就执行垃圾回收。

CLR使用引用跟踪算法。引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括类的静态和实例字段,或者方法的参数和局部变量。引用类型的变量称为-根。

CLR开始GC时,首先暂停进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改其状态。然后,CLR进入GC的标记阶段。在这个阶段,CLR遍历堆中的所有对象,将同步块索引字段中的一位设为0。这表明所有对象都应删除。然后,CLR检查所有活动根,查看它们引用了哪些对象。如果一个根包含null,CLR忽略这个根并继续检查下个根。

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

已标记的对象是可达的,因为应用程序代码可通过仍在引用它的变量抵达(访问)它。未标记的对象是不可达的,因为应用程序中不存在使对象能再次访问的根。

CLR知道哪些对象可以幸存,哪些可以删除后,就进入GC的压缩阶段。在这个阶段,CLR对堆中已标记的对象进行“移动”,压缩多有幸存下来的对象,使它们占用连续的内存空间。最后,CLR还要从每个根减去所引用的对象在内存中便宜的字节数。这样就能保证么个根还是引用和之前一样的对象。压缩好内存之后,托管堆的NextObjPtr指针指向最后一个幸存对象之后的位置。下一个分配的对象将放到这个位置。

如果CLR在一次GC之后回收不了内存,而且进程中没有空间来分配新的GC区域。就说明该进程的内存已耗尽。

静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止。会导致内存泄露,应避免使用静态字段

1.3 垃圾回收和调试

一旦根离开作用域,它引用的对象就变得“不可达”,GC会回收其内存;不保证对象在方法的生存期中自始至终地存活。

21.2 代:提升性能

CLR的GC是基于代的垃圾回收器

对象越新,生存期越短。

对象越老,生存期越长。

回收堆的一部分,速度快于回收整个堆。

托管堆在初始化时不包含对象。添加到堆的对象称为第0代(新构造的对象)对象,垃圾回收器从未检查过它们。

托管堆只支持三代:第0代,第1代和第2代。

如果垃圾回收器发现在回收0代后存活下来的对象很少,就可能减少第0代的预算。已分配空间的减少意味着垃圾回收将更频繁地发生,但垃圾回收器每次做的事情也减少了,这减少了进程的工作集。

如果垃圾回收器回收了第0代,发现还有很多对象存活,没有多少内存被回收,就会增大第0代的预算。现在,垃圾回收的次数将减少,但每次进行垃圾回收时,回收的内存要多得多。

 2.1 垃圾回收出发条件

代码显示调用System.GC的静态Collect方法。

Window报告低内存情况。

CLR正在卸载AppDomain。

CLR正在关闭

2.2 大对象

85000字节或更大的对象时大对象。CLR以不同的方式对待大小对象

大对象不是小对象的地址空间分配,而是在进程地址空间的其他地方分配。

目前版本的GC不压缩大对象,因为在内存中移动它们代价过高。

大对象总是第2代,绝不可能是第0代或第1代。所以只能为需要长时间存活的资源创建大对象。大对象一般是大字符串(比如XML或JSON)或者用于I/O操作的字节数组(比如从文件或网络将字节读入缓冲区以便处理)。

2.3 垃圾回收模式

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

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

服务器:该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。GC假定机器上没有运行其他应用程序,并假定机器的所有CPU都可用来辅助完成GC。该模式造成托管堆被拆分成几个区域,每个CPU一个。

子模式:并发(默认) 或非并发模式。在并发模式中,垃圾回收器有一个额外的后台线程,他能在应用程序运行时并发标记对象。

GCSetting类中GCLatencyMode属性:

符号名称 说明
Batch(“服务器“GC模式的默认值) 关闭并发GC
Interractive(“工作站”GC模式默认值) 打开并发GC
LowLatency 在短期的,时间敏感的操作中使用这个延迟模式
SustainedLowLatency 使用这个延迟模式,应用程序的大多数操作都不会发生长的GC暂停

 

 2.4 强制垃圾回收

GCCollectionMode枚举

符号名称 说明
Default 等同于不传递任何符号名称。目前还等同于传递Forced
Forced 强制回收指定的代(以及低于它的所有代)
Optimized 只有在能释放大量内存或者能减少碎片化的前提下,才执行回收。0000000000000000000000000000000

 

2.5 监视应用程序的内村官使用

21.3 使用需要特殊清理的类型

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

 CriticalFinalizerObjectl类:

首次构造任何CriticalFinalizerObject派生类型的对象时,CLR立即对继承层次结构的所有Finalize方法进行JIT编译。

CLR是在调用了非CriticalFinlizerObject派生类型的Finalize方法之后,才调用CriticalFinalizerObject派生类型的Finalize方法。

如果AppDomain被一个宿主应用程序强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。

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

3.2 一个有趣的依赖性问题

3.3 GC为本机资源提供的其他功能

3.4 终结的内部工作原理

应用程序创建新对象时,new操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表中。终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象——回收该对象的内存前应调用它的Finalize方法。

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

CLR为每个AppDomain都提供了一个GC句柄表,允许应用程序监视或手动控制对象的生存期。这个表在AppDomain创建之初时空白的。表中每个记录项包含以下两种信息:对托管堆中的一个对象的引用,以及指出如果监视或控制对象的标识。

GCHandleType枚举类型:

Weak:该标志允许监视对象的生存期。

WeakTrackResurrection:该标志允许监视对象的生存期。

Normal:该标志允许控制对象的生存期。

Pined:该标志允许控制对象的生存期。

垃圾回收发生时,垃圾回收器的行为:

1.垃圾回收器标记所有可达的对象。然后,垃圾回收器扫描GC句柄表:所有Normal或Pinned对象都被看成是根,同时标记这些对象(包括这些对象通过它们的字段引用的对象)。

2.垃圾回收器扫描GC句柄表,查找所有Weak记录项。如果一个Weak记录项引用了未标记的对象,该引用标识的就是不可达对象,该记录项的引用值更改为null。

3.垃圾回收器扫描终结列表。在列表中,对未标记的引用标识的是不可达对象,这些引用从终结列表移至freachable队列。这时对象会被标记,因为对象又变成可达了。

4.垃圾回收器扫描GC句柄表,查找所有WeakTrackResurrection记录项。

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

每天学习一丢丢
原文地址:https://www.cnblogs.com/terry-1/p/10451404.html