转自:http://www.cnblogs.com/yuyijq/archive/2009/08/09/1542435.html

股票里面有个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. 1: [ComVisible(true), SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
  1. 2: public abstract class CriticalFinalizerObject
  1. 3: {
  1. 4: // Methods
  1. 5: [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
  1. 6: protected CriticalFinalizerObject()
  1. 7: {
  1. 8: }
  1. 9: 
  1. 10: [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
  1. 11: ~CriticalFinalizerObject()
  1. 12: {
  1. 13: }
  1. 14: }

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

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

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

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

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

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

先来一个普通的类型

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

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

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

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

前面一部分就不再列了

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

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

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

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

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

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

注意到,这里的_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

.NET中的那些受特别对待的类型(CriticalFinalizerObject)的更多相关文章

  1. 从C#程序中调用非受管DLLs

    从C#程序中调用非受管DLLs 文章概要: 众所周知,.NET已经渐渐成为一种技术时尚,那么C#很自然也成为一种编程时尚.如何利用浩如烟海的Win32 API以及以前所编写的 Win32 代码已经成为 ...

  2. iOS 中可用的受信任根证书列表

    iOS 中可用的受信任根证书列表 iOS 受信任证书存储区中包含随 iOS 一并预装的受信任根证书. 关于信任和证书 以下所列的各个 iOS 受信任证书存储区均包含三类证书: “可信”的证书用于建立信 ...

  3. selenium向IE的输入框中输入字符时特别慢

    selenium向IE的输入框中输入字符时特别慢,需要去selenium官网下载32位的iedriver,替换掉64位的,即可解决.

  4. Go 语言中的方法,接口和嵌入类型

    https://studygolang.com/articles/1113 概述 在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题: 编译器会 ...

  5. pojo类对应的就是数据库中的表,pojo类属性类型一定要用包装类Integer等

    pojo类对应的就是数据库中的表,pojo类属性类型一定要用包装类Integer等 pojo类对应的就是数据库中的表,pojo类属性类型一定要用包装类Integer等 pojo类对应的就是数据库中的表 ...

  6. C#中方法的参数的四种类型

    C#中方法的参数有四种类型:       1. 值参数类型  (不加任何修饰符,是默认的类型)       2. 引用型参数  (以ref 修饰符声明)       3. 输出型参数  (以out 修 ...

  7. 领域模型中的实体类分为四种类型:VO、DTO、DO、PO

    http://kb.cnblogs.com/page/522348/ 由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念: 概念: V ...

  8. Python中,如何初始化不同的变量类型为空值

    参考文章  Python中,如何初始化不同的变量类型为空值 常见的数字,字符,很简单,不多解释. 列表List的其值是[x,y,z]的形式 字典Dictionary的值是{x:a, y:b, z:c} ...

  9. Access中出现改变字段“自己主动编号”类型,不能再改回来!(已解决)

    Access中出现改变字段"自己主动编号"类型,不能再改回来! (已解决) 一次把access中的自增字段改成了数值,再改回自增时,提示:在表中输入了数据之后,则不能将不论什么字段 ...

随机推荐

  1. byte[]与各种数据类型互相转换示例

    public class TestCase { /** * short到字节数组的转换. */ public static byte[] shortToByte(short number) { int ...

  2. PHP中用下划线开头的含义

    命名的规则 加一个为私有的 加两个一般都是系统默认的,系统预定义的,即所谓:=====================“魔术方法”与“魔术常量”=====================★PHP起止为 ...

  3. angularjs控制器之间的数据共享与通信

    1.可以写一个service服务,从而达到数据和代码的共享; var app=angular.module('app',[]); app.service('ObjectService', [Objec ...

  4. Dubbo通过注解实现RPC调用

    启动Dubbo服务有2个方式,1是通过xml配置,2是通过注解来实现,这点和Spring相似. 采用XML配置如下:  <?xml version="1.0" encodin ...

  5. panda 函数-处理空值

    今天这里谈的函数,以后进行数据分析的时候会经常用到. import numpy as npimport pandas as pdfrom pandas import DataFrame , Serie ...

  6. C#/JAVA 程序员转GO/GOLANG程序员笔记大全(DAY 00)

    一.安装说明 https://studygolang.com/dl 二.环境变量 // 下载 *.msi 安装文件,部分环境变量默认配置好了. 其他配置如下描述 三.目录及项目层级关系 在系统环境变量 ...

  7. MYSQL freedata 外联接

    主要是解决,不同生产系统里面,有不同的数据库. SQL 又不能夸系统查询表. 只能在一个系统里,可以跨不同的数据库查表. 所以会用映射 .FREEDATA 这种方式,这样A 系统 里的表更新之后,就可 ...

  8. zoj 2976 Light Bulbs(暴力枚举)

    Light Bulbs Time Limit: 2 Seconds      Memory Limit: 65536 KB Wildleopard had fallen in love with hi ...

  9. 201621123006 《Java程序设计》第4周学习总结

    1. 本周学习总结 1.1 写出你认为本周学习中比较重要的知识点关键词 多态.重载.继承.覆盖.super.抽象类 1.2 尝试使用思维导图将这些关键词组织起来.注:思维导图一般不需要出现过多的字. ...

  10. APUE学习笔记——10信号——信号接口函数 signal 和 sigaction

    signal函数     signal函数是早起Unix系统的信号接口,早期系统中提供不可靠的信号机制.在后来的分支中,部分系统使用原来的不可靠机制定义signal函数,如 Solaris 10 .而 ...