zoukankan      html  css  js  c++  java
  • .NET中的那些受特别对待的类型(CriticalFinalizerObject)

    股票里面有个ST股,就是Special Treatment的意思。就是对那些财务出现异常的上市公司,特别处理,在股票名字前面挂个ST,警示投资者注意风险。

    这是题外话,今天我们要谈的是,在.NET的世界里,也有这么一些类型啊,受特别的对待(世界的不公平无处不在啊)。当EE碰到这些类型时,并不是像普通的类型那样去对待。我“龌龊”的给这些类型起个名字: ST Type。那到底有哪些类型呢,就我目前所知道的有:

    CriticalFinalizerObject

    MarshalByRefObject

    ContextBoundObject

    ValueType

    Array

    String

    Enum

    上面几个是在CLR层面上的,也就是这几个类型深入到核心了,会影响到CLR对这些类型的处理行为。

    下面就分别对这几个类型的具体作用做一些简单的描述:

    首先是CriticalFinalizerObject类型,该类型在System.Runtime.ConstrainedExecution命名空间下,属于mscorlib.dll程序集。

    其实CriticalFinalizerObject类型非常简单:

       1: [ComVisible(true), SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
       2: public abstract class CriticalFinalizerObject
       3: {
       4:     // Methods
       5:     [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
       6:     protected CriticalFinalizerObject()
       7:     {
       8:     }
       9:  
      10:     [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
      11:     ~CriticalFinalizerObject()
      12:     {
      13:     }
      14: }

    简单的居然是空的,不过这个类型有点与一般的类型不同的是,这个类型有个终结方法。大部分类型是没有定义终结方法的(注意,我说的是大部分哦)。如果你不知道给一个类型定义一个终结方法有什么影响,请翻翻MSDN,速查一下。

    就这样一个几乎是空的类型,有些什么作用呢?

    终结器方法的JIT编译行为不同

    要解释这个类型的作用,我们先来了解一下终结方法是干啥的,为什么要定义一个终结方法。我们定义一个终结方法一般是这个类型“消费”了一些本地资源,这些资源是非托管的,也就是CLR管不了。那类型的开发者就必须显式的销毁这些资源,那我们就给这个类型定义一个终结方法(至于是否该定义终结方法,终结方法到底该怎么用,不在本文讨论之列)。当GC确定一个类型是垃圾的时候,就会着手清理这个类型,但是又发现这个类型实现了一个终结方法,GC就会把这些对象放到一个可终结对象的队列里面,然后一一调用这些对象的终结方法,在调用这些终结方法之前,JIT肯定先要编译这个终结方法。但是你想啊,为什么这个时候有GC,除了你手动的调用System.Collect()方法外,那肯定是有新的对象要分配,但是堆中空间不足,既然空间不足,那JITCompiler是否有足够的资源来编译这个方法都是一个问题,那如果没有足够的资源编译这个终结方法,终结方法也就无法调用,这样就造成终结方法内部要销毁的非托管资源也无法释放了。这就会引起资源泄露,资源泄露是一个很大的问题,会造成应用程序运行的不稳定。

    上面这一段好像陷入了一种非常矛盾的境地,那有啥办法确保终结方法一定会被JIT呢?那就是CriticalFinalizerObject登场了。

    一切直接或间接的从CriticalFinalizerObject派生的类型,在创建的时候JIT就会将这些个类型的终结方法先给编译了。这下好了,你要回收内存的时候可能没有资源即时编译终结方法,那你创建的时候总有资源吧。为了证明这点我们用Visual Studio + SOS来一探究竟,先上示例代码:

    先来一个普通的类型

       1: using System;
       2: using System.Runtime.ConstrainedExecution;
       3:  
       4: public class Program
       5: {
       6:     static void Main()
       7:     {
       8:         Foo f = new Foo();
       9:         //待会儿就在这里下个断点吧
      10:         f.Test();
      11:  
      12:         Console.ReadLine();
      13:     }
      14: }
      15:  
      16: //普通的类型,直接从System.Object继承
      17: public class Foo
      18: {
      19:     public void Test()
      20:     { 
      21:     
      22:     }
      23:  
      24:     ~Foo()
      25:     { 
      26:     
      27:     }
      28: }

    设好断点,F5调试(记得在VisualStudio的工程属性,调试那一栏里选择上“允许非托管代码调试”),然后在立即窗口里输入:

       1: .load sos.dll
       2: extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
       3: !dso
       4: PDB symbol for mscorwks.dll not loaded
       5: OS Thread Id: 0xfd8 (4056)
       6: ESP/REG  Object   Name
       7: 0012f16c 01312db8 Foo
       8: 0012f220 01312db8 Foo
       9: 0012f224 01312db8 Foo
      10: 0012f440 01312db8 Foo
      11: 0012f444 01312db8 Foo
      12:  
      13: !do 01312db8
      14: Name: Foo
      15: MethodTable: 00993090
      16: EEClass: 00991358
      17: Size: 12(0xc) bytes
      18:  (E:\Study\ConsoleApplication9\ConsoleApplication9\bin\Debug\ConsoleApplication9.exe)
      19: Fields:
      20: None
      21:  
      22: !dumpmt -md 00993090
      23: EEClass: 00991358
      24: Module: 00992c5c
      25: Name: Foo
      26: mdToken: 02000003  (E:\Study\ConsoleApplication9\ConsoleApplication9\bin\Debug\ConsoleApplication9.exe)
      27: BaseSize: 0xc
      28: ComponentSize: 0x0
      29: Number of IFaces in IFaceMap: 0
      30: Slots in VTable: 6
      31: --------------------------------------
      32: MethodDesc Table
      33:    Entry MethodDesc      JIT Name
      34: 79286a70   79104934   PreJIT System.Object.ToString()
      35: 79286a90   7910493c   PreJIT System.Object.Equals(System.Object)
      36: 79286b00   7910496c   PreJIT System.Object.GetHashCode()
      37: 0099c038   00993078     NONE Foo.Finalize()
      38: 0099c040   00993084      JIT Foo..ctor()
      39: 0099c030   00993068     NONE Foo.Test()

    (对命令稍做解释,.load sos.dll加载sos模块,!dso就是DumpStackObjects,输出栈上所有的对象,然后找到我们的测试类型Foo的地址为01312db8,使用!do 01312db8,输出堆中这个对象的一些信息,我们这里需要的是方法表MethodTable的地址00993090,然后使用!dumpmt –md 00993090输出这个方法的方法表)
    在最后列出的方法表的条目里,我们发现终结方法Foo.Finalize(注意,虽然在C#里,终结方法的定义是在类型名称前面加一个~,但是经过C#编译器后,这个方法在IL里就是Finalize)这个时候没有被JIT。好了,我们看看如果是从CriticalFinalizerObject派生呢?

       1: using System;
       2: using System.Runtime.ConstrainedExecution;
       3:  
       4: public class Program
       5: {
       6:     static void Main()
       7:     {
       8:         Foo f = new Foo();
       9:         f.Test();
      10:  
      11:         Console.ReadLine();
      12:     }
      13: }
      14:  
      15: public class Foo : CriticalFinalizerObject
      16: {
      17:     public void Test()
      18:     { 
      19:     
      20:     }
      21:  
      22:     ~Foo()
      23:     { 
      24:     
      25:     }
      26: }

    代码几乎一模一样,真的有什么不同么:

    前面一部分就不再列了

       1: !dumpmt -md 00993090
       2: EEClass: 00991360
       3: Module: 00992c5c
       4: Name: Foo
       5: mdToken: 02000003  (E:\Study\ConsoleApplication9\ConsoleApplication9\bin\Debug\ConsoleApplication9.exe)
       6: BaseSize: 0xc
       7: ComponentSize: 0x0
       8: Number of IFaces in IFaceMap: 0
       9: Slots in VTable: 6
      10: --------------------------------------
      11: MethodDesc Table
      12:    Entry MethodDesc      JIT Name
      13: 79286a70   79104934   PreJIT System.Object.ToString()
      14: 79286a90   7910493c   PreJIT System.Object.Equals(System.Object)
      15: 79286b00   7910496c   PreJIT System.Object.GetHashCode()
      16: 0099c038   00993078      JIT Foo.Finalize()
      17: 0099c040   00993084      JIT Foo..ctor()
      18: 0099c030   00993068     NONE Foo.Test()

    呀,这个时候Finalize真的已经被JIT了呢。可这个时候明明类型才刚刚初始化啊,离调用终结方法很远啊,这就是CriticalFinalizeObject的作用。

    终结器方法的执行顺序不同

    除了上面这一点以外啊,还有就是,CLR会先调用不是从CriticalFinalizerObject派生的类型的终结方法,然后再调用从CriticalFinalizerObject类型派生的类型终结方法。这样有什么好处呢?这样就确保了在普通的类型里的终结方法里,一定可以访问得到从CriticalFinalizerObject类型派生的类型,以免造成很微妙的现象。

    比如文件流FileStream类型,它的终结器方法如下所示:

    ~FileStream()
    {
        if (this._handle != null)
        {
            this.Dispose(false);
        }
    }

    注意到,这里的_handle字段是SafeFileHandle类型的,而SafeFileHandle的继承树是:

    SafeFileHandle->SafeHandleZeroOrMinusOneIsInvalid->SafeHandle->CriticalFinalizerObject,这样在FileStream的终结方法执行时,就会确保_handle还没有被垃圾回收了。

    终结器方法必定会执行

    除此之外,即使是AppDomain被宿主给卸载了,从CriticalFinalizerObject派生的类型的终结方法还是会被执行,这样确保了非托管资源一定会得到回收。

    有人肯定会说:你说了这么多,可在我的编程生涯中根本就没碰到过CriticalFinalizerObject类型啊。确实,我们几乎没有碰到过这个类型,更没有自己要实现从这个类型派生的类型。这是为什么呢?实际上这一切都被微软给封装起来了,同在mscorlib.dll程序集内,System.Runtime.InteropServices命名空间下有一个类型SafeHandle,因为对一些非托管资源来说基本上都是一些句柄,所以SafeHandle提供了一种统一的处理方式,来处理这些句柄。然后,对于所有的非托管资源在类库里基本上都有对应的SafeHandle的派生类,你可以在Microsoft.Win32.SafeHandles找到这些句柄。还有一些其他的句柄在FCL中,但它们都是internal的,没有开放给我们开发人员,比如在System.Data.SqlClient下的SNIHandle表示与数据库连接的句柄。

    参考资料

    《CLR via C#》

    MSDN

    本来想把前面列出的四个类型都写一下,最后发现第一个类型就写的比较多,最后就决定将作为多篇叙说。而且,就我目前知道的只有这四个类型,如果谁还知道其他的类型或我日后了解到更多内容,我会一并总结如此。

    声明:该文章虽为我所写,但主要是总结,大部分资料“抄袭”自上面两个参考资料,然后我再加工处理,添加些代码和Debug示例。算是半原创吧。特此声明。

    祝编程愉快

  • 相关阅读:
    加班的价值
    webApp 阅读器项目实践
    Oak Seeds 网站项目回顾
    [Echarts]用Echarts绘制饼状图
    [转载] 编程每一天(Write Code Every Day)
    对杀毒软件技术的浅浅理解
    记我的第二次自动化尝试——selenium+pageobject+pagefactory实现自动化下单、退款、撤销回归测试
    学习Selenium遇到的问题和解决方案
    记我的第一次自动化尝试
    jmeter环境配置、使用以及参数化之CSV Data Set Config
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1542435.html
Copyright © 2011-2022 走看看