.net gc 垃圾回收机制

源于Clr Via C#

在什么时候回收

CLR 在检测第 0 代超过预算时触发一次 GC。这是 GC 最常见的触发条件,下面列 出其他条件:

  1. 代码显式调用 System.GC 的静态 Collect 方法。代码可显式请求 CLR 执行回收。虽然 Microsoft 强烈反对这种请求,但有时情势比人强。详情参见本章稍后的 21.2.4 节“强 制垃圾回收”。
  2. Windows报告低内存情况。CLR内部使用 Win32 函数 CreateMemoryResourceNotification 和 QueryMemoryResourceNotification 监视系统的总体内存使用情况。如果 Windows 报 告低内存,CLR 将强制垃圾回收以释放死对象,减小进程工作集。
  3. CLR 正在卸载 AppDomain。一个 AppDomain 卸载时,CLR 认为其中一切都不是根,所以 执行涵盖所有代的垃圾回收。AppDomain 将在第 22 章讨论。
  4. CLR 正在关闭。CLR 在进程正常终止42时关闭。关闭期间,CLR 认为进程中一切都不是根。 对象有机会进行资源清理,但 CLR 不会试图压缩或释放内存。整个进程都要终止了, Windows 将回收进程的全部内存。 

以什么逻辑执行回收

0代最早:对象越新,生存期越短。对象越老,生存期越长。回收堆的一部分,速度快于回收整个堆 。

回收时进行compact,大对象不compact。

可以以服务器模式工作站模式(配置文件中配置,runtime下的<gcServer enabled=”false”/>),并发非并发(runtime节点下的<gcConcurrent enabled=”true”/>)执行回收。

终结器,也就是“析构函数”,实际编译为Finalize方法,CLR特殊对待,在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表 (finalization list)中,在标识对象过程中将引用从“终结队列”移入“freachable队列”(此时对象不再被认为是垃圾,不能回收它的内存。对象被视为垃圾又变得不是垃圾,我们说对象被复活了),后由特殊的高优先级专用线程逐一调用freachable队列中对象的Finalize方法并清空freachable队列。可终结对象在回收时必须存活,造成它被提升到另一代,下次对老一代进行垃圾回收时,会发现已终结的对象成为真正的垃圾,因为没有应用程序的 根指向它们,freachable 队列也不再指向它们。

如果拥有非托管资源,又需要人为控制,就实现IDisposable接口的Dispose()方法。需要说明的是,一般我们都定义一个Dispose(bool disposing)方法,这不是IDisposable接口要求实现的,这样做的原因是给终结器和IDisposable接口以统一的逻辑,在IDisposable.Dispose中调用Dispose(false),在终结器中调用Dispose(true),参数disposing表示是否由CLR在释放对象前调用的,所以在Dispose(bool disposing)方法类,当参数为false时我们应该仅释放非托管资源,保证对象自身(对象自己是托管对象)还是可用的。

文档将 disposal 和 dispose 翻译成“释放”。这里解释一下为什么不赞成这个翻译。在英语中,这个词的意思是“摆脱”或“除 去”(get rid of)一个东西,尤其是在这个东西很难除去的情况下。之所以认为“释放”不恰当,除了和 release 一词冲突,还 因为 dispose 强调了“清理”和“处置”,而且在完成(对象中包装的)资源的清理之后,对象占用的内存还暂时不会释放。所 以,“dispose 一个对象”真正的意思是:清理或处置对象中包装的资源(比如它的字段引用的对象),然后等着在一次垃圾回 收之后回收该对象占用的托管堆内存(此时才释放)。

重要提示:定义实现 IDisposable 接口的类型时,在它的所有方法和属性中,一定要在对象 被显式清理之后抛出一个 System.ObjectDisposedException。而 Dispose 方法永远不要抛出ObjectDisposedException;被多次调用就直接返回。

重要提示:我一般不赞成在代码中显式调用 Dispose。理由是 CLR 的垃圾回收器已经写得非常好,应该放心地把工作交给它。垃圾回收器知道一个对象何时不再由应用程序代码访问, 而且只有到那时才会回收对象。53而当应用程序代码调用 Dispose 时,实际是在信誓旦旦地 说它知道应用程序在什么时候不需要一个对象。但许多应用程序都不可能准确知道一个对象 在什么时候不需要。
例如,假定在方法 A 的代码中构造了一个新对象,然后将对该对象的引用传给方法 B。方法B可能将对该对象的引用保存到某个内部字段变量(一个根)中。但方法 A并不知道这个情况,它当然可以调用Dispose。但在此之后,其他代码可能试图访问该对象,造成抛出一个ObjectDisposedException。建议只有在确定必须清理资源(例如删除打开的文件)时才调用 Dispose。 也可能多个线程同时调用一个对象的 Dispose。但 Dispose 的设计规范指出 Dispose不一定要线程安全。原因是代码只有在确定没有别的线程使用对象时才应调用 Dispose。

Finalize 方法问题较多,使用须谨慎。记住它们是为释放本机资源而设计的。强烈建议不要重写 Object 的 Finalize 方法。相反,使用 Microsoft 在 FCL 中提供的辅助类。这些辅助类重写了 Finalize 方法并添加了一些特殊的 CLR“魔法”。你从这些辅助类派生出自己的类,从而继承 CLR 的“魔法” 。创建封装了本机资源的托管类型时,应先从 System.Runtime.InteropServices.SafeHandle 这个 特殊基类派生出一个类。

SafeHandle 类有两点需要注意。

其一,它派生自 CriticalFinalizerObject;CriticalFinalizerObject 在 System.Runtime.ConstrainedExecution 命名空间定义。CLR 以特殊方式对待这个类及其派生类。具体地说,CLR 赋予这个类以下三个很酷的功能。

  1. 首次构造任何 CriticalFinalizerObject 派生类型的对象时,CLR 立即对继承层次结构中的所有 Finalize 方法进行 JIT 编译。构造对象时就编译这些方法,可确保当对象被确定为垃圾之后,本机资源肯定会得以释放。不对 Finalize 方法进行提前编译,那么也许能分配并使用本机资源,但无法保证释放。内存紧张时,CLR 可能找不到足够的内存来 编译 Finalize 方法,这会阻止 Finalize 方法的执行,造成本机资源泄漏。另外,如果 Finalize 方法中的代码引用了另一个程序集中的类型,但 CLR 定位该程序集失败,那 么资源将得不到释放。
  2. CLR 是在调用了非 CriticalFinalizerObject 派生类型的 Finalize 方法之后,才调用 CriticalFinalizerObject 派生类型的 Finalize 方法。这样,托管资源类就可以在它们的 Finalize 方法中成功地访问 CriticalFinalizerObject 派生类型的对象。例如,FileStream 类的 Finalize 方法可以放心地将数据从内存缓冲区 flush49到磁盘,它知道此时磁盘文件 还没有关闭。
  3. 如果 AppDomain 被一个宿主应用程序(例如 Microsoft SQL Server 或者 Microsoft ASP.NET)强行中断,CLR 将调用 CriticalFinalizerObject 派生类型的 Finalize 方法。 宿主应用程序不再信任它内部运行的托管代码时,也利用这个功能确保本机资源得以释 放。

其二,SafeHandle 是抽象类,必须有另一个类从该类派生并重写受保护的构造器、抽象方 法 ReleaseHandle 以及抽象属性 IsInvalid 的 get 访问器方法。

如何以配置式以及编程式来影响GC的默认逻辑

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

编程时可以使用GC,GCSettings,GCHandle影响GC行为。

GC.AddMemoryPressure(long)和GC.RemoveMemoryPressure(long)可成对用来控制期间的GC回收行为(在内部,GC.AddMemoryPressure 和 HandleCollector.Add 方法会调用 GC.Collect, 在第 0 代超过预算前强制进行 GC)。

HandleCollector(string name,int threshold)用来表示当threshold个HandleCollector(使用HandleCollector的实例方法Add来人为增加这个HandleCollector实例存在的数量)存在于堆中时,它(HandleCollector的实例)就执行回收。

如何让GC不回收某些对象

CLR 为每个 AppDomain 都提供了一个 GC 句柄表(GC Handle table),允许应用程序监视或手 动控制对象的生存期。这个表在 AppDomain 创建之初是空白的。表中每个记录项都包含以下两种信息:对托管堆中的一个对象的引用,以及指出如何监视或控制对象的标志(flag)。 应用程序使用如下所示的 System.Runtime.InteropServices.GCHandle 类型在表中添加或删除记录项。

简单地说,为了控制或监视对象的生存期,可调用 GCHandle 的静态 Alloc 方法并传递想控
制/监视的对象的引用。还可传递一个 GCHandleType,这是一个标志,指定了你想如何控制 /监视对象。GCHandleType 是枚举类型,它的定义如下所示:

public enum GCHandleType
{
    Weak = 0,      // 用于监视对象的存在     
    WeakTrackResurrection = 1,  // 用于监视对象的存在  
    Normal = 2,      // 用于控制对象的生存期     
    Pinned = 3      // 用于控制对象的生存期 
}

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

“弱引用”可用WeakReference<T>,ConditionalWeakTable来简化编程

 

 

 

发表评论

电子邮件地址不会被公开。