DoubleJ

DoubleJ 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

DoubleJ 发布了文章 · 1月18日

反射

元数据

元数据是用一系列表来存储的,生成一个程序集或模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表以及其它表。

反射

程序运行的时候解析这些元数据表以获取信息,该行为便是反射。反射允许在运行时发现并使用编译时还不了解的类型及其成员。

反射的性能
  • 反射会造成编译时无法保证类型安全性。反射需要重度依赖字符串,所以会丧失编译时的类型安全。例如执行Type.GetType("A"),在一个程序集中查找类型名为“A”的类型,但程序集实际包含的类型可能是“AA”,代码会通过编译,但运行时会出错。
  • 反射速度慢,使用反射时,类型及其成员的名称在编译时未知,要使用字符串名称标识每个类型及其成员,以便在运行时发现它们。也就是说在扫描程序集的元数据时,反射要不断的执行字符串的搜索,而字符串的搜索执行的是不区分大小写的比较,这会进一步影响速度。
反射调用一个成员时也会对性能产生影响

反射调用方法时,首先必须将实参打包成一个数组,在内部反射必须将这些实参解包到线程栈上。此外在调用方法前,CLR必须检查实参具有正确的数据类型,最后CLR还需确保调用者有正确的安全权限来访问被调用的成员。

*综上所述,最好避免使用反射技术来访问字段或者调用方法。

获取Type对象的几种方式
  • Type的静态方法GetType:接收一个string参数,必须指定类型的全名(包括命名空间),如果调用程序集没有定义指定的类型,就查找MSCorLib.dll定义的类型,如果还是没找到就返回null
  • Type的静态方法ReflectionOnlyGetType:行为与GetType相似,只是类型会加载到一个“仅反射”上下文中,不能执行
构造类型的实例
  • Activator的静态方法CreateInstance:调用该方法可以传递一个Type对象引用,也可以传递标识了想要创建的类型的一个String。其中直接获取一个类型对象的几个重载版本相对简单,为类型的构造器传递一组实参,方法返回的是对新对象的一个引用。而使用字符串来指定所需类型的几个重载版本要稍微复杂一些,首先必须指定另一个字符串来标识定义了类型的那个程序集。其次这些版本返回的不是对新对象的引用,而是一个System.Runtime.Remoting.ObjectHandle对象。ObjectHandle类型允许将一个AppDomain中创建的对象传至其它AppDomain,期间不强迫对象具体化,要具体化该对象可以调用ObjectHandle对象的Unwrap方法。在一个AppDomain中调用这个方法时,它会将定义了要具体化的类型的程序集加载到这个AppDomain中。如果对象按引用封送就创建代理类型和对象。如果按值封送,对象的副本会被反序列化。
  • Activator的静态方法CreateInstanceFrom:行为与CreateInstance方法相似,不同的是必须通过字符串来指定类型及其程序集。程序集要使用Assembly的LoadFrom(而非Load)方法加载到调用的AppDomain中。由于没有接收Type参数的版本,所以返回的都是ObjectHandle对象的引用,必须调用ObjectHandle的Unwrap方法进行具体化
构造泛型类型的实例
static void Main()
{
    //获取泛型类型的类型对象的一个引用
    Type temp = typeof(List<>);

    //使用int类型封闭泛型类型
    Type closedType = temp.MakeGenericType(typeof(int));

    //构造封闭类型的实例
    object o = Activator.CreateInstance(closedType);

    Console.WriteLine(o.GetType());
}

运行结果
image.png

发现类型成员

字段、构造器、方法、属性、事件和嵌套类型都可以被定义为类型的成员。FCL包含一个System.Reflection.MemberInfo的类型,封装了一组所有类型都通用的属性。层次结构图如下:
image.png

查询一个类型的成员并显示与之相关的一些信息
static void Main()
{
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    //遍历AppDomain中加载的所有程序集
    foreach (var item in assemblies)
    {
        Console.WriteLine("Assembly: {0}", item);

        //查找程序集中的类型
        foreach (var t in item.GetExportedTypes())
        {
            Console.WriteLine("Type: {0}", t);

            //发现类型成员
            const BindingFlags bf = BindingFlags.DeclaredOnly |
                BindingFlags.NonPublic | BindingFlags.Public |
                BindingFlags.Instance | BindingFlags.Static;

            foreach (var mi in t.GetMembers(bf))
            {
                var typeName = string.Empty;
                if (mi is Type)
                    typeName = "Type";
                else if (mi is FieldInfo)
                    typeName = "FieldInfo";
                else if (mi is MethodInfo)
                    typeName = "MethodInfo";
                else if (mi is ConstructorInfo)
                    typeName = "ConstructorInfo";
                else if (mi is PropertyInfo)
                    typeName = "PropertyInfo";
                else if (mi is EventInfo)
                    typeName = "EventInfo";

                Console.WriteLine("mi typeName: {0}", typeName);
            }
        }
    }
}
BindingFlags枚举(筛选返回的成员类型)
  • Default:指定未定义任何绑定标志
  • IgnoreCase:返回与指定字符串匹配的成员(忽略大小写)
  • DeclaredOnly:只返回被反射的那个类型的成员,忽略继承的成员
  • Instance:返回实例成员
  • Static:返回静态成员
  • Public:返回公共成员
  • NonPublic:返回非公共成员
  • FlattenHierarchy:返回基类型定义的公共成员和受保护静态成员

遍历反射对象模型图
image.png

基于一个AppDomain可发现加载到其中的所有程序集。基于一个程序集可发现构成它的所有模块。基于一个程序集或模块可发现它定义的所有类型。基于一个类型可发现它的嵌套类型、字段、构造器、方法、属性和事件

发现类型的接口
interface ITest1 : IDisposable
{
    void Method1();
    void Method2();
}

interface ITest2
{
    void Method1();
}

class MyClass : ITest1, ITest2, IDisposable
{
    //ITest1
    void ITest1.Method1() { }

    public void Method2() { }

    //ITest2
    void ITest2.Method1() { }

    //IDisposable
    public void Dispose() { }

    //非接口方法
    public void Method1() { }
}

class Program
{
    static void Main()
    {
        //查找MyClass实现的接口
        Type t = typeof(MyClass);
        Type[] interfaces = t.FindInterfaces(TypeFilter, typeof(Program).Assembly);

        //显示每个接口的信息
        foreach (var item in interfaces)
        {
            Console.WriteLine("接口: {0}", item);

            //获取接口映射
            InterfaceMapping map = t.GetInterfaceMap(item);

            for (int i = 0; i < map.InterfaceMethods.Length; i++)
            {
                Console.WriteLine("方法 {0} 在 {1} 中实现", map.InterfaceMethods[i], map.TargetMethods[i]);
            }
        }

        Console.ReadKey();
    }

    private static bool TypeFilter(Type t, object filterCriteria)
    {
        //如果接口是由filterCriteria标识的程序集中定义的就返回true
        return t.Assembly == (Assembly)filterCriteria;
    }
}

结果
image.png

调用类型的成员

Type类提供了一个InvokeMember方法,可以通过这个方法调用一个类型的成员。在调用这个方法时,会在类型的成员中搜索一个匹配的成员,如果没有找到则会抛出一个异常。反之则会调用成员,该成员返回什么InvokeMember方法也会返回一个同样的信息数据。

InvokeMember使用的BindingFlags
image.png

一次绑定多次调用

使用InvokeMember方法的弊端在于每次调用该方法,都必须先绑定到一个特定的成员然后才能调用,这大大地增加了耗时。可以直接调用Type的某个方法来绑定成员(GetFields,GetMethods等),可以返回对一个对象的引用,通过对象引用直接访问特定成员。

示例代码

class SomeType
{
    private int m_SomeValue;
    public int SomeValue
    {
        get
        {
            return this.m_SomeValue;
        }

        set
        {
            this.m_SomeValue = value;
        }
    }

    public string SomeMethod()
    {
        Console.WriteLine("Run SomeMethod");
        return "SomeMethod Rusult";
    }
}

static void Main()
{
    var bf = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    Type t = typeof(SomeType);
    //构造Type实例
    object o = t.InvokeMember(null, bf | BindingFlags.CreateInstance, null, null, null);
    Console.WriteLine("o type : {0}", o.GetType().ToString());

    //读写字段
    t.InvokeMember("m_SomeValue", bf | BindingFlags.SetField, null, o, new object[] { 1 });
    int v = (int)t.InvokeMember("m_SomeValue", bf | BindingFlags.GetField, null, o, null);
    Console.WriteLine("m_SomeValue = {0}", v);

    //调用一个方法
    string methodRes = (string)t.InvokeMember("SomeMethod", bf | BindingFlags.InvokeMethod, null, o, null);
    Console.WriteLine("methodRes = {0}", methodRes);

    Console.WriteLine("\n---------分割线---------\n");

    //构造实例
    ConstructorInfo ctor = t.GetConstructor(new Type[] { });
    o = ctor.Invoke(null);

    //读写字段
    FieldInfo fi = o.GetType().GetField("m_SomeValue", bf);
    fi.SetValue(o, 2);
    Console.WriteLine("m_SomeValue = {0}", fi.GetValue(o));

    //调用方法
    MethodInfo mi = o.GetType().GetMethod("SomeMethod", bf);
    methodRes = (string)mi.Invoke(o, null);
    Console.WriteLine("methodRes = {0}", methodRes);

    Console.ReadKey();
}

结果
image.png

使用句柄减少进程中内存的消耗

由于Type和MemberInfo的派生对象需要大量内存,因此如果应用程序容纳了太多这样的对象,但只是偶尔调用一下,内存消耗将急剧增长,对性能产生负面影响。

解决方法:如果需要大量缓存Type和MemberInfo的派生对象,我们可以使用允许时句柄来替代对象,从而减小工作集。FCL定义了三个运行时句柄类型,分别是:RuntimeTypeHandle,RuntimeFieldHandle,RuntimeMethodHandle,三个类型均属于值类型,只包含一个IntPtr字段,这个字段是一个句柄,引用了AppDomain的Loader堆中的一个类型、字段或方法。

转化方法:

  • Type转RuntimeTypeHandle:调用Type的静态方法GetTypeHandle
  • RuntimeTypeHandle转Type:调用Type的静态方法GetTypeFromHandle
  • FieldInfo转RuntimeFieldHandle:查询FieldInfo实例只读字段FieldHandle
  • RuntimeFieldHandle转FieldInfo:调用FieldInfo的静态方法GetFieldFromHandle
  • MethodInfo转RuntimeMethodHandle:查询MethodInfo实例只读字段MethodHandle
  • RuntimeMethodHandle转MethodInfo:调用MethodInfo的静态方法GetMethodFromHandle

示例代码

private static void Show(string s)
{
    Console.WriteLine("堆大小 : {0,12:##,###,###} - {1}", GC.GetTotalMemory(true), s);
}

static void Main()
{
    var bf = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

    Show("任何反射操作之前的堆的大小");

    //为MSCorlid.dll中所有方法构建MethodInfo对象缓存
    List<MethodBase> methodBases = new List<MethodBase>();
    foreach (var item in typeof(Object).Assembly.GetExportedTypes())
    {
        if (item.IsGenericTypeDefinition)
            continue;

        MethodBase[] mb = item.GetMethods(bf);
        methodBases.AddRange(mb);
    }
    Console.WriteLine("方法个数 : {0}", methodBases.Count);
    Show("绑定所有方法后堆的大小");

    //为所有MethodInfo对象构建RuntimeMethodHabdle缓存
    List<RuntimeMethodHandle> runtimeMethodHandles = methodBases.ConvertAll<RuntimeMethodHandle>(mb => mb.MethodHandle);
    Show("构建RuntimeMethodHandle后堆的大小");

    //阻止缓存被过早垃圾回收
    GC.KeepAlive(methodBases);

    //垃圾回收
    methodBases = null;
    Show("垃圾回收methodBases后堆的大小");

    Console.ReadKey();
}

结果
image.png

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 1月14日

AppDomain

什么是AppDomain

AppDomain是一组程序集的逻辑容器,CLR初始化时创建的第一个AppDomain称为默认AppDomain,默认的AppDomain只有在Windows进程终止时才会被销毁。

AppDomain作用
  • 一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问。一个AppDomain中的代码创建一个对象后,该对象被该AppDomain拥有。这个对象的生存期不可能比创建该对象的代码所在的AppDomain生存期长。一个AppDomain中的代码要访问另一个AppDomain中的对象,必须使用“按引用封送”或者“按值封送”的语义。从而加强AppDomain之间的边界,使得两个不同的AppDomain之间不存在对象之间的直接引用。所以可以很容易的从一个进程中卸载AppDomain而不会影响到其它应用程序中正在运行的代码
  • AppDomain可以卸载,CLR不支持卸载AppDomain中的单个程序集,可以卸载AppDomain从而卸载包含在该AppDomain中的所有程序集
  • Appdomain可以单独保护,AppDomain在创建之后会应用一个权限集,权限集决定了向AppDomain中运行的程序集授予的最大权限。保证当宿主加载一些代码后,这些代码不会破坏宿主本身使用的一些重要数据结构
  • AppDomain可以单独实施配置,AppDomain在创建之后会关联一组配置设置,这些设置主要影响CLR在AppDomain中加载程序集的方式。如:搜索路径、版本绑定重定向、卷影复制以及加载器优化
Windows进程图

image.png

上图所示有两个APPdomain,分别为AppDomain#1(默认AppDomain)和AppDomain#2。其中AppDomain#1包含3个程序集:MyApp.exe,TypeLib.dll,System.dll。AppDomain#2包含2个程序集:Wintellect.dll和System.dll。

System.dll被加载到两个程序集中,假设两个AppDomain都使用了来自System.dll中的同一个类型,那么在两个AppDomain的Loader堆中都会为同一个类型分配一个类型对象,类型对象的内存不会由两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型调用的方法时,方法的IL代码会进行JIT编译,生成的本地代码将与每一个AppDomain关联,方法的代码不由调用它的所有AppDomain共享。

虽然不共享类型对象的内存或者本地代码是一种浪费,但是AppDomain的全部目的是提供隔离性。CLR要求在卸载某个AppDomain并释放它的所有资源的同时,不会对其它AppDomain产生负面影响。

有些程序集本来就会被多个AppDomain使用,如MSCorLib.dll,该程序集包含了System.Object,System.Int32以及其它所有与.NET Framework密不可分的类型。CLR初始化时,该程序集会自动加载,而且所有AppDomain都共享该程序集中的类型。为了减少资源消耗,该程序集通过“AppDomain中立”的方式加载,CLR会为它们维护一个特殊的Loader堆,该Loader堆中的所有类型对象以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。

*共享资源的代价:“AppDomain中立”的方式加载的所有程序集永远不能被卸载,为了回收它们占用的资源唯一的方法便是终止Windows进程,让Windows回收资源。

跨AppDomain访问对象
  • 按引用封送
//按引用封送
public class MarshalByRefType : MarshalByRefObject
{
    public MarshalByRefType()
    {
        Console.WriteLine("{0} 在 {1} 中执行", this.GetType().ToString(), Thread.GetDomain().FriendlyName);
    }

    public void SomeMethod()
    {
        Console.WriteLine("SomeMethod 在 {0} 中执行", Thread.GetDomain().FriendlyName);
    }

    public MarshalByValType MethodWithReturn()
    {
        Console.WriteLine("MethodWithReturn 在 {0} 中执行", Thread.GetDomain().FriendlyName);
        MarshalByValType t = new MarshalByValType();
        return t;
    }

    public NonMarshalableType MethodArgAndReturn(string callDomainName)
    {
        Console.WriteLine("AppDomain {0} 调用 AppDomain {1}", callDomainName, Thread.GetDomain().FriendlyName);
        NonMarshalableType t = new NonMarshalableType();
        return t;
    }
}
  • 按值封送
//按值封送
[Serializable]
public class MarshalByValType : Object
{
    private DateTime m_CreateTime = DateTime.Now;

    public MarshalByValType()
    {
        Console.WriteLine("{0} 在 {1} 中执行,创建于 {2}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, this.m_CreateTime);
    }

    public override string ToString()
    {
        return this.m_CreateTime.ToLongDateString();
    }
}
  • 完全不能封送类型
//该实例无法跨AppDomain传送
public class NonMarshalableType : Object
{
    public NonMarshalableType()
    {
        Console.WriteLine("创建NonMarshalableType 在 {0} 中执行", Thread.GetDomain().FriendlyName);
    }
}

调用代码

 class Program
{
    static void Main(string[] args)
    {
        //获取AppDomain引用
        AppDomain appDomain = Thread.GetDomain();

        //获取AppDomain名称
        string appDomainName = appDomain.FriendlyName;
        Console.WriteLine("默认AppDomain FriendlyName = {0}", appDomainName);

        //获取AppDomain中包含Main方法的程序集
        string exeAssembly = Assembly.GetEntryAssembly().FullName;
        Console.WriteLine("Main assembly = {0}", exeAssembly);

        //定义局部变量引用AppDomain
        AppDomain appDomain1 = null;

        //按引用封送
        Console.WriteLine("{0} Demo 1", Environment.NewLine);

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain1", null, null);
        MarshalByRefType mbrt = null;

        //将程序集加载到新的AppDomain中,构造对象把它封送到新建的AppDomain(实际得到一个代理引用)
        mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");
        Console.WriteLine("Type = {0}", mbrt.GetType());

        //证明得到的是代理对象的引用
        Console.WriteLine("是代理对象 = {0}", RemotingServices.IsTransparentProxy(mbrt));

        //看起来像是在MarshalByRefType上调用一个方法,其实是在代理类型上调用方法
        //代理使线程转至拥有对象的那个AppDomain,并在真实的对象上调用这个方法
        mbrt.SomeMethod();

        //卸载创建的AppDomain
        AppDomain.Unload(appDomain1);

        //mbrt引用一个有效的代理对象,代理对象引用一个无效的AppDomain
        try
        {
            //在代理对象上调用一个方法,AppDomain无效抛出异常
            mbrt.SomeMethod();
            Console.WriteLine("调用成功");
        }
        catch (AppDomainUnloadedException)
        {
            Console.WriteLine("调用失败");
        }

        //按值封送
        Console.WriteLine("{0} Demo 2", Environment.NewLine);

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain2", null, null);

        //将程序集加载到新的AppDomain中,构造对象把它封送到新建的AppDomain(实际得到一个代理引用)
        mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");

        //对象的方法返回对象的一个副本,对象按值传送
        MarshalByValType mbvt = mbrt.MethodWithReturn();

        //证明得到的不是代理对象的引用
        Console.WriteLine("是代理对象 = {0}", RemotingServices.IsTransparentProxy(mbvt));

        //看起来像是在MarshalByRefType上调用一个方法,实际也是如此
        Console.WriteLine(mbvt.ToString());

        //卸载创建的AppDomain
        AppDomain.Unload(appDomain1);

        //mbvt引用有效的对象,卸载AppDomain没有影响
        try
        {
            //不会抛出异常
            Console.WriteLine(mbvt.ToString());
            Console.WriteLine("调用成功");
        }
        catch (AppDomainUnloadedException)
        {
            Console.WriteLine("调用失败");
        }

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain3", null, null);

        //将程序集加载到新的AppDomain中,构造对象把它封送到新建的AppDomain(实际得到一个代理引用)
        mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");

        Console.WriteLine("{0} Demo 3", Environment.NewLine);

        //对象的方法返回一个不可封送的对象,抛出异常
        try
        {
            NonMarshalableType nmt = mbrt.MethodArgAndReturn(appDomainName);
        }
        catch (Exception e)
        {

            Console.WriteLine(e.Message);
        }

        Console.ReadKey();
    }
}

运行结果
image.png

代码分析:
首先获得一个AppDomain对象的引用,当前调用线程正在这个AppDomain中执行。由于多个AppDomain可以在一个Windows进程中,所以线程能执行一个AppDomain中的代码再执行另一个AppDomain中的代码。从CLR的角度看线程一次只能执AppDomain中的代码。

AppDomain创建之后可以赋予它一个友好名称用来标识AppDomain,CLR使用可执行文件的文件名来作为默认的AppDomain的友好名称。

按引用封送
调用CreateDomain告诉CLR在同一个进程中创建一个新的AppDomain,新的AppDomain有自己的Loader堆(目前是空的),因此还没有程序集被加载到Loader中。创建AppDomain时CLR不在这个AppDomain中创建任何线程,AppDomain中也没有代码运行,除非显示的让一个线程调用AppDomain中的代码。

为了在新的AppDomain中创建一个新类型的实例,首先必须将一个程序集加载到AppDomain中,然后构建该程序集中定义的一个类型的实例。CreateInstanceAndUnwrap做的便是这个事情,该方法接收两个参数,第一个参数表示AppDomain要加载的程序集,第二个参数表示想要构建实例对象的类型名称。在内部该方法将导致调用线程从当前AppDomain转至新的AppDomain,现在线程将指定的程序集加载到新的AppDomain中,并扫描程序集的类型定义元数据表,查找指定的类型(MyAppDomain.MarshalByRefType"),找到类型后创建该类型实例,线程返回默认的AppDomain,使得CreateInstanceAndUnwrap能返回对新的MarshalByRefType对象的引用。

由于CLR并不允许一个AppDomain中的变量引用另一个AppDomain中创建的对象,因此在CreateInstanceAndUnwrap方法返回对象的引用之前还需要执行一些额外的逻辑。

引用返回之前的额外工作
MarshalByRefType类型是从System.MarshalByRefObject派生的,这个类是一个非常特殊的基类,当CreateInstanceAndUnwrap发现自己封送的一个对象的类型派生自MarshalByRefObject时,CLR就会跨AppDomain边界按引用封送对象。

源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR会在目标AppDomain的Loader堆中定义一个代理类型,这个代理类型是用原始类型的元数据定义的。因此它看起来和原始类型完全一样:有完全一样的实例成员(属性、事件和方法)。但是实例字段不会成为代理类型的一部分。

在目标AppDomain中定义好代理类型后,CreateInstanceAndUnwrap方法会创建这个代理类型的一个实例,初始化它的字段来标识AppDomain和真实对象,然后将这个代理对象的引用返回目标AppDomain。调用RemotingServices.IsTransparentProxy证明返回的确实是一个代理对象。

接着引用程序使用代理来调用SomeMethod方法,由于mbrt引用一个代理对象,所以会调用由代理实现的SomeMethod方法。在代理的调用中,利用了代理对象中的信息字段,将调用线程从默认AppDomain切换至新的AppDomain。现在该线程的任何行为都在新AppDomain的安全策略和配置下执行。然后线程使用代理对象的GCHandle字段查找新AppDomain中的真是对象,并用真是对象调用真是的SomeMethod方法。

*一个AppDomain中的线程调用另一个AppDomain中的方法时,线程会在两个AppDomain中进行切换,这也意味着跨AppDomain边界的方法调用是同步的。任意时刻一个线程只能在一个AppDomain中

紧接着调用Unload方法强制CLR卸载指定的AppDomain,并强制执行一次垃圾回收,释放由卸载的AppDomain中的代码创建的所有对象。此时默认的AppDomain还引用着一个有效的代理对象,但是代理对象不再引用一个有效的AppDomain。此时再试图调用SomeMothed时,调用的是该方法在代理中的实现,代理发现真实对象的AppDomain已经卸载,所以抛出异常。

按值封送
代码与按引用封送类似,不同的是MarshalByValType不是从MarshalByRefObject派生的,所以CLR不能定义一个代理类型,并创建一个代理类型的实例。对象不能按引用封送,但是由于MarshalByValType标记了[Serializable],所以CreateInstanceAndUnwrap能够按值封送对象。

源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR将对象的实例字段序列化成一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain,然后CLR在目标AppDomain中反序列化字节数组,这个操作会强制CLR将定义了被反序列化的类型的程序集加载到目标AppDomain中。接着CLR创建类型的一个实例,并利用字节数组中的值初始化对象的字段,使之与源对象中的值相同。然后CreateInstanceAndUnwrap返回对这个副本的引用。如此便实现了对象的跨AppDomain边界按值封送。

到此源AppDomain中的对象和目标AppDomain中的对象就有了独立生存期,它们的状态可以独立地更改。如果源AppDomain中没有根保持源对象地存活,源对象的内存会在下一次垃圾回收时被回收。

接下来程序使用真实对象调用ToString方法,由于mbrt引用一个真实的对象,所以会调用这个方法的真实实现,线程不会在AppDomain之间切换。

为了进一步证明不是代理对象,现在将AppDomain卸载,继续调用ToString方法,调用仍然成功。

不可封送类型
由于NonMarshalableType类型既没有派生自MarshalByRefObject也没有[Serializable]标记,所以不能按引用封送也不能按值封送,对象完全不能跨AppDomain边界进行封送。同时抛出一个SerializationException异常。

监视AppDomain

可以将AppDomain的静态属性MonitoringIsEnabled设置为true,从而监视AppDomain的资源消耗情况。

示例代码

class AppDomainMonitorDelta : IDisposable
{
    private AppDomain m_AppDomain;
    private TimeSpan m_ThisADCpu;
    private long m_ThisADMemoryInUse;
    private long m_ThisAdMemoryAllocated;

    static AppDomainMonitorDelta()
    {
        //打开AppDomain监视
        AppDomain.MonitoringIsEnabled = true;
    }

    public AppDomainMonitorDelta(AppDomain appDomain)
    {
        this.m_AppDomain = appDomain ?? AppDomain.CurrentDomain;
        this.m_ThisADCpu = this.m_AppDomain.MonitoringTotalProcessorTime;
        this.m_ThisADMemoryInUse = this.m_AppDomain.MonitoringSurvivedMemorySize;
        this.m_ThisAdMemoryAllocated = this.m_AppDomain.MonitoringTotalAllocatedMemorySize;
    }

    public void Dispose()
    {
        GC.Collect();
        Console.WriteLine(
            "FriendlyName={0}, CPU={1}ms",
            this.m_AppDomain.FriendlyName, (this.m_AppDomain.MonitoringTotalProcessorTime - this.m_ThisADCpu).TotalMilliseconds
        );
        Console.WriteLine(
            "Allocated {0:N0} bytes of which {1:N0} survived GCs",
            this.m_AppDomain.MonitoringTotalAllocatedMemorySize - this.m_ThisAdMemoryAllocated,
            this.m_AppDomain.MonitoringSurvivedMemorySize - this.m_ThisADMemoryInUse
        );
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (new AppDomainMonitorDelta(null))
        {
            //分配回收时会存活的约10M字节
            var list = new List<Object>();
            for (int i = 0; i < 1000; i++)
                list.Add(new Byte[10000]);

            //分配回收时不会存活的约20M字节
            for (int i = 0; i < 2000; i++)
                new Byte[10000].GetType();

            //保持CPU工作约5秒
            long stop = Environment.TickCount + 5000;
            while (Environment.TickCount < stop) ;
        }

        Console.ReadKey();
    }
}

输出结果
image.png

AppDomain类的4个只读属性

  • MonitoringSurvivedProcessMemorySize:Int64属性,返回由当前CLR实际控制的所有AppDomain正在使用的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringTotalAllocatedMemorySize:Int64属性,返回一个特定的AppDomain已分配的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringSurvivedMemorySize:Int64属性,返回一个特定的AppDomain当前正在使用的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringTotalProcessorTime:TimeSpan属性,返回一个特定的AppDomain的CPU占用率
AppDomain卸载

卸载AppDomain会导致CLR卸载AppDomain中的所有程序集,还会释放AppDomain的Loader堆。可以调用AppDomain的静态方法Unload卸载AppDomain。

卸载AppDomain时CLR执行的一系列操作

  • CLR挂起进程中执行过托管代码的所有线程
  • CLR检查所有线程栈,查看有哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时刻返回至要卸载的那个AppDomain。在任何一个栈上,如果有准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异常并同时恢复线程的执行。这将导致线程展开,在展开的过程中执行遇到的所有finally块中的代码,以进行资源清理。如果没有代码捕捉ThreadAbortException异常,它会成为一个未处理的异常,并且CLR会吞噬该异常。线程会终止,但进程会继续运行(这一点非常特殊,因为对于其它所有未处理的异常CLR都会终止进程)
  • 当上一步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由已卸载的AppDomain创建的对象”的每一个代理对象都设置一个标志。这些代理对象现在知道它们引用的真实对象已经不存在了,如果任何代码试图调用一个无效的代理对象上的方法,该方法会抛出AppDomainUnloadException
  • CLR强制垃圾回收,对现已卸载AppDomain创建的任何对象占用的内存进行回收。并调用这些对象的Finalize方法,彻底清理对象所占用的资源
  • CLR恢复所有剩余线程的执行,调用AppDomain.Unload方法的线程继续运行(AppDomain.Unload的调用是同步进行的)

*如果调用AppDomain.Unload方法的线程正好在要卸载的AppDomain中,CLR会创建一个新的线程来尝试卸载AppDomain。第一个线程被强制抛出ThreadAbortException并展开,新建的线程将等待AppDomain卸载,然后新线程终止。

FirstChance异常通知

给AppDomain的实例事件FirstChanceException添加委托可以在捕捉到异常的时候获得回调。

CLR异常处理:异常首次抛出时,CLR会调用已向抛出异常的那个AppDomain登记的FirstChanceException回调方法。然后CLR查找栈上在同一个AppDomain中的任何catch块,如果有一个catch块能处理异常,则异常处理完成,程序继续正常执行。如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上调用AppDomain,再次抛出同一个异常对象。CLR会继续调用已向当前AppDomain登记的FirstChanceException回调方法,该过程会一直持续,知道抵达线程栈的顶部。如果异常还未被任何代码处理,CLR将终止整个进程。

*FirstChanceException只负责监视AppDomain抛出异常时获取一个通知,回调方法并不能处理异常

可执行应用程序执行过程

Windows通过一个托管EXE文件初始化一个进程时,会加载垫片。垫片会检查包含在EXE文件中的CLR头信息。头信息指明生成和测试应用程序时使用的CLR版本(垫片根据这个信息决定哪个版本的CLR加载到进程中),CLR加载并初始化好之后,它会检查程序集的CLR头,判断应用程序的入口是哪个(Main方法),CLR调用这个方法使应用程序真正启动并运行。
代码运行时会访问其它类型,引用另一个程序集的类型时CLR会定位所需的程序集,并把它加载到同一个AppDomain中。当应用程序的Main方法返回后,Windows进程终止并销毁默认的AppDomain和其它所有AppDomain。

*可调用System.Environment.Exit方法关闭Windows进程,该方法能保证所有对象的Finalize方法被执行

当托管代码出现错误时,CLR可以做什么?
  • 如果一个线程的执行时间过长,CLR可以终止线程并返回一个响应
  • CLR可以卸载AppDomain,从而卸载有问题的代码
  • CLR可以被禁用,阻止更多的托管代码在程序中运行
  • CLR可以退出Windows进程(先终止所有线程后卸载所有AppDomain)
宿主如何拿回它的线程?

宿主应用程序一般都要保持对自己线程的控制,以数据库服务为例:新请求抵达数据库,线程A获得该请求,后把该请求派发给线程B执行实际工作。假设线程B要执行的代码进入无限循环,这将导致数据库服务器派发的线程B一去不复返了,如此服务器是不是应该创建更多的线程,而这些线程本身也可能进入无限循环。

宿主可利用线程终止功能解决上述问题,线程终止工作方式如图:
image.png

  • 1、客户端向服务器发送一个请求
  • 2、服务器接到该请求并把它一个线程池来执行实际工作
  • 3、线程池线程获得该请求,开始执行可信代码
  • 4、可信代码进入try块,跨越AppDomain边界调用代码(包含不可信代码)
  • 5、宿主在接到客户端的请求时会记录一个时间,如果不可信代码在设定的时间期限内没有做出响应,宿主就会调用Thread的Abort方法终止线程,强制抛出ThreadAbortException
  • 6、线程开始展开,调用finally块进行清理工作。最终线程池线程穿越AppDomain返回。由于宿主代码是从一个try块中调用不可信代码的,所以宿主有一个catch块捕捉ThreadAbortException异常
  • 7、为了响应捕捉到的ThreadAbortException异常,宿主调用Thread的ResetAbort方法
  • 8、由于宿主的代码已经捕捉到了ThreadAbortException异常,因此宿主可以向客户端返回某种形式的错误,允许线程池线程返回线程池,供未来的新请求使用

ThreadAbortException是一个比较特殊的异常,即使代码捕捉了该异常,CLR也不允许将该异常吞噬,即在catch块的尾部CLR会重新抛出该异常。同时支持调用Thread的ResetAbort方法告诉CLR不需要在catch的尾部重新抛出ThreadAbortException异常。而调用ResetAbort方法时要求调用者被授予SecurityPermission权限并且ControlThread标志被设置为true,但是宿主在为不可信代码创建AppDomain时不会向其授予该权限,这样便能保证不可信代码不可能自行处理并吞噬该异常,从而保证宿主能正常的捕捉到ThreadAbortException异常,重新获取该线程的控制权,并把它重新放回到线程池中。

*当线程从它的ThreadAbortException展开时,不可信代码可以执行catch块的finally块,在这些块中代码可能进入无限循环从而组织宿主重新获取线程的控制权。这时候宿主应用程序可以用过卸载AppDomain、禁用CLR、终止进程的方式来修复这个问题。如果不可信代码捕捉了ThreadAbortException异常并且重新抛出一个新的异常类型,如果这个新的异常被捕捉到CLR会在catch的尾部自动重新抛出ThreadAbortException异常。

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 1月13日

程序集加载

加载过程

JIT编译器将IL代码编译成本地代码时,会查看IL代码引用了哪些类型。在运行时,JIT编译器利用程序集的TypeRef和AssemblyRef元数据表来确定哪一个程序集定义了所引用的类型。在AssemblyRef元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT编译器获取所有这些部分:名称(无扩展名和路径)、版本、语言文化、公钥标记(如果被加载的程序集是弱命名的,那么标识中只包含:程序集的名称,将不再包含版本、语言文化、公钥标记信息),并把它们连接成一个字符串。然后CLR尝试将与该标识匹配的一个程序集加载到AppDomain中(如果还没加载的话)。在内部CLR通过System.Reflection.Assembly类的静态方法Load来尝试加载此程序集(可以显示的将一个程序集加载到AppDomain中)。

Assembly的Load方法
  • 加载强命名程序集

Load方法导致CLR向程序集应用一个版本绑定重定向策略,并在GAC(全局程序集缓存)中查找对应的程序集。如果没找到,就接着去应用程序的基目录、私有路径子目录和codebase位置查找。

  • 加载弱命名程序集

和加载强命名程序集不同的是,Load不会向应用程序应用版本绑定重定向策略,CLR也不会去GAC中查找程序集。

最后如果Load找到指定的程序集,就会返回已加载的程序集的一个Assembly对象的引用,如果没有加载到指定程序集,则会抛出一个System.IO.FileNotFoundException。

AppDomain的Load方法(尽量不使用)

与Assembly的Load方法不同,该方法属于实例方法。作用为将一个程序集加载到一个指定的AppDomain中。可供非托管代码调用,允许宿主将一个程序集注入一个指定的AppDomain。托管代码一般不使用该方法,因为在调用该方法后会开始在常规位置搜寻程序集,而AppDomain关联了一些设置告诉CLR如何查找程序集,为了加载程序集CLR将使用与指定AppDomain关联的设置,而不是发出调用的那个AppDomain关联的设置。以及AppDomain的Load方法返回对程序集的一个引用,由于System.Assembly类不是从System.MarshalByRefObject派生的,所以程序集必须按值封送回那个发起调用的AppDomain。现在CLR就会用发出调用的AppDomain的关联设置来定位并加载程序集。如果发出调用的AppDomain没有找到指定的程序集就会抛出FileNotFountException。这不是我们期望的行为,所以尽量避免使用AppDomain的Load方法。

Assembly的LoadFrom方法

在内部LoadFrom会先调用System.Reflection.AssemblyName类的静态方法GetAssemblyName,该方法打开指定的文件,查找AssemblyRef元数据表的记录项,提取程序集标识信息,然后返回System.Reflection.AssemblyName对象并关闭文件。随后LoadFrom方法在内部调用Assembly的Load方法,将AssemblyName对象传给它。接着CLR会应用版本绑定重定向策略,并在各个位置查找程序集。如果Load找到了匹配的程序集就会加载并返回已加载程序集的一个Assembly对象,LoadFrom方法将返回这个值。如果没有找到匹配的程序集,LoadFrom就会加载通过LoadFrom的实参传递的路径中的程序集。如果已加载了一个具有相同标识的程序集,LoadFrom方法会简单返回代表已加载程序集的一个Assembly对象。

*可以给LoadFrom方法传递一个URL实参,CLR会主动下载文件并将其安装到用户的下载缓存中,最后再从那加载文件

Assembly的LoadFile方法

调用这个方法可以从任意路径加载一个程序集,并可将具有相同标识的一个程序集多次加载到一个AppDomain中。通过LoadFile加载程序集时,CLR不会自动解析任何依赖问题,代码必须向AppDomain的AssemblyResolve事件登记,让事件回调方法显示加载任何依赖的程序集。

Assembly的ReflectionOnlyLoadFrom方法和ReflectionOnlyLoad方法
  • ReflectionOnlyLoadFrom:加载由路径指定的文件,文件的强名称标识不会获取,也不会再GAC和其它位置搜索文件。
  • ReflectionOnlyLoad:会在GAC、应用程序基目录、私有路径和codebase指定的位置搜索指定的程序集。和Load不同的是不会应用版本控制策略,即指定的什么加载版本,获得的便是哪个版本。
  • ReflectionOnlyLoadFrom和ReflectionOnlyLoad:这两个方法加载程序集时,CLR都会禁止程序集中的任何代码执行。试图执行这两个方法加载的程序集中的代码,都会导致CLR抛出InvalidOperationException。
存在多个具有相同标识的程序集

一台机器上可能同事存在多个具有相同标识的程序集,由于LoadFrom会在内部调用Load方法,所以CLR有可能不加载你指定的文件,而是加载一个不同的文件,从而达不到预期效果。建议每次生成程序集都修改版本号,确保每个版本都有自己的唯一标识,进而确保LoadFrom方法能达到预期行为。

程序集卸载

CLR并未提供单独卸载某个程序集的功能,因为一旦这种行为被允许,那么一旦线程从某个方法返回至已卸载的程序集中的代码,应用程序就会崩溃。要卸载程序集必须卸载程序集所在的整个AppDomain。而使用ReflectionOnlyLoadFrom方法和ReflectionOnlyLoad方法加载的程序集由于程序集中的代码不允许被执行,让人看上去这些程序集是可以卸载的,实际上CLR也不允许卸载这两个方法加载的程序集,因为即便不会运行程序集中的代码但仍然可以利用反射来创建对象,引用程序集中定义的元数据,如果程序集被卸载就必须使这些对象无效,从实现复杂度还是执行速度上来讲跟踪这些对象的状态都是得不偿失的。

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月4日

CLR寄宿

CLR寄宿

.NET Framework在Windows平台的顶部运行,意味着.NET Framework必须用Windows可以理解的技术构建:所有托管模块和程序集文件都必须使用Windows PE文件格式,要么是一个Windows EXE文件,要么是一个DLL文件。

CLRCreateInstance函数

定义在程序集MSCorEE.dll中,该程序集一般被成为垫片,该文件一般在C:\Windows\System32目录中。它的工作是负责决定创建哪个版本的CLR。

一台机器可以安装多个版本的CLR,但是只有一个版本的MSCorEE.dll文件。机器上安装的MSCorEE.dll版本是与安装的最新版的CLR一起发布的那个版本。

CLRCreateInstance函数可以返回一个ICLRMetaHost接口,可以调用该接口中的GetRuntime函数指定宿主要创建的CLR版本。然后垫片将所需版本的CLR加载到宿主的进程中。

GetRuntime函数返回一个指向非托管接口ICLRRuntimeInfo的指针,通过调用GetInterface方法获得ICLRRuntimeHost接口,可利用该接口做以下事情:

  • 设置宿主管理器:告诉CLR内存分配、线程调度/同步、程序集加载等决策。宿主还可以声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知
  • 获取CLR管理器:告诉CLR阻止使用某些类/成员。宿主还能分辨哪些代码可以调式而哪些代码不能调试,以及一些特定事件(AppDomain卸载、堆栈溢出异常)发生时宿主应该调用哪些方法
  • 初始化并启动CLR
  • 加载一个程序集并执行其中的代码
  • 停止CLR,阻止任何更多的托管代码在Windows进程中运行

*Windows进程完全可以不加载CLR,只有需要在进程中执行托管代码时才需要加载它

CLR寄宿的好处
  • 可以用任何编程语言来编写
  • 代码在JIT编译后执行而不是一边解释一边执行,所以速度很快
  • 代码使用垃圾回收避免内存泄漏和损坏
  • 代码在一个安全的沙箱中运行
  • 宿主不必操心提供一个丰富的开发环境。宿主可以利用现有的技术,包括语言、编译器、编辑器、调试器、profiler等
查看原文

赞 0 收藏 0 评论 0

DoubleJ 关注了用户 · 1月4日

高阳Sunny @sunny

SegmentFault 思否 CEO
C14Z.Group Founder
Forbes China 30U30

独立思考 敢于否定

曾经是个话痨... 要做一个有趣的人!

任何问题可以给我发私信或者发邮件 sunny@sifou.com

关注 2148

DoubleJ 发布了文章 · 2020-12-31

垃圾回收(GC)

工作原理

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

  • 调用IL指令newobj,为代表资源的类型分配内存。在C#中使用new关键字,编译器会自动生成该指令
  • 初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器负责初始状态
  • 访问类型的成员来使用资源
  • 摧毁资源的状态以进行清理
  • 释放内存,由垃圾回收负责

这看似简单的步骤,却频频引发编程错误,如使用了已被释放的内存和没有释放不再需要的内存。

在非托管编程中,这种bug会造成内存泄漏(浪费内存)和对象损坏的问题,如何正确的进行内存管理便是一个很重要的问题,而垃圾回收便是专门负责这一功能的。

从托管堆分配内存

CLR要求所有资源都从托管堆分配,对象在应用程序不需要使用时便会被自动删除。

进程初始化时,CLR要保留一块连续的、最初并没有对应的物理存储空间地址空间,这个地址空间就是托管堆。托管堆维护着一个指向下一个对象在堆中分配的位置的指针NextObjPtr。刚开始的时候NextObjPtr设置为保留地址空间的基地址。

IL指令newobj用于创建一个对象,该指令会让CLR执行以下步骤:

  • 计算类型以及所有基类型的字段需要的字节数
  • 加上对象开销所需要的字节数,每个对象都具有的两个开销字段:类型对象指针和一个同步索引块。对于32位应用程序两个字段各占32位需要8字节,64位应用程序两个字段各占64位需要16字节
  • CLR检查保留区域是否能够提供分配对象所需的字节数,如果有必要就提交存储。如果托管堆有足够的空间,对象会被放入,对象将会放在NextObjPtr指针指向的地址中,并且为它分配的字节将会被清零。接着调用类型的实例构造器位this参数传递NextObjPtr,newobj指令将返回对象的地址。在地址返回前,NextObjPtr指针的值会加上对象占据的字节数从而得到一个新值,它指向下一个对象放入托管堆的地址

image.png

垃圾回收器工作原理

应用程序调用new操作符创建对象时,可能存在没有足够的内存空间来分配该对象的情况,托管堆将对象需要的字节数加到NextObjPtr指针中的地址上来检测这种情况,如果结果值超过了地址空间的末尾,表明托管堆已满,此时必须执行一次垃圾回收。

垃圾回收算法

垃圾回收器检查应用程序中是否存在不再使用的对象,如果存在,它们使用的内存就可以回收,如果一次垃圾回收之后,堆中任然没有可用的内存,new操作符将会抛出一个OutOfMemoryException异常。

应用程序的root

每个应用程序都包含一组根,每个根都是一个存储位置,其中包含一个指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null

*只有引用类型的变量才被认为是根,值类型的变量永远不认为是根

垃圾回收器如何知道应用程序正在使用一个对象?
  • 标记阶段:垃圾回收器在开始执行时,总是假设堆中所有的对象都是垃圾。垃圾回收器沿着线程栈上行检查所有的根,如果发现一个根引用了一个对象,就在对象的同步索引字段上设置一个bit。例如应用程序的根直接引用了A,B,C对象,所有这些对象都被标记。在标记对象C的时候发现对象C引用了对象D的一个字段,造成对象D也被标记。垃圾回收器就是这样以递归的方式遍历所有可达对象。
  • 标记好根和它的引用对象后,垃圾回收器检查下一个根,并继续标记对象,如果试图标记一个先前已经标记过的对象,就会沿着这个路径走下去,不会进行二次遍历
  • 检查好所有根之后,堆中将包含一组已标记和未标记的对象。已标记的对象是通过应用程序代码可达的对象,而未标记的对象是不可达的认为是垃圾,它们占用的内存可以回收。垃圾回收器将进入压缩阶段
  • 压缩阶段:垃圾回收器线性地遍历堆,寻找未标记对象的连续内存块,如果发现的内存块较小,垃圾回收器就将其忽略。但是如果发现大的可连续的内存块,垃圾回收器就会把非垃圾对象移动到这里以压缩堆。
  • 移动内存中的对象之后,包含指向这些对象的指针的变量和CPU寄宿器都将无效。垃圾回收器必须重新访问应用程序的所有根,并修改它们使其指向对象的新的内存位置。如果对象中的字段指向的是一个已经移动了位置的对象,垃圾回收器也要负责改正这些字段。堆内存压缩之后,NextObjPtr指针指向紧接着最后一个非垃圾对象之后的位置

*由此可见,垃圾回收将造成显著的性能损失

使用终结操作释放本地资源

大多数类型只需要内存便可正常工作,但是还有一些类型除了要使用类型还要使用本地资源,如FileStream类型。
终结是CLR提供的一种机制,允许对象在垃圾回收器回收其内存之前执行一些清理操作。这些类型都实现了一个Finalize方法,当垃圾回收器判定一个对象是垃圾时,会调用对象的Finalize方法。

Finalize方法的定义

class People
{
    //这是一个Finalize方法
    ~People()
    {
        //这里的代码会进入Finalize方法
    }
}

通过ildasm检查程序集可确认Finalize方法已生成
image.png

CriticalFinalizerObject类型
  • 首次构造任何CriticalFinalizerObject派生类型的一个对象时,CLR立即对继承层次结构中的所有Finalize方法进行JIT编译,在构造对象就编译这些方法可确保对象被判定为垃圾的时候本地资源能保证得到释放
  • CLR会先调用非CriticalFinalizerObject派生类性的Finalize方法,再调用CriticalFinalizerObject派生类型的Finalize方法。如此托管类资源便可以在它们的Finalize方法中成功的访问CriticalFinalizerObject派生类型的对象
  • 如果AppDomain被一个宿主应用程序强行中断,CLR也会调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序不再信任它内部运行的托管代码时也利用这个功能确保本地资源得以释放
引起Finalize方法调用的原因
  • 第0代满时,会自动触发垃圾回收,导致Finalize方法被调用的最常见的原因
  • 代码显示调用System.GC的静态方法Collect,不建议这样操作
  • Windows提示内存不足
  • CLR卸载AppDomain,卸载时CLR会认为AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收
  • CLR关闭,一个进程正常终止时,CLR就会关闭。在关闭过程中,CLR会认为该进程中不再存在任何根,因此会调用托管堆中的所有对象的Finalize方法。此时由于整个进程都要终止,CLR不会尝试压缩或释放内存,将有Windows负责回收进程的所有内存
使用Finalize方法造成的性能影响
  • 可终结的对象要花更长的时间分配内存,因为指向它们的指针必须放到终结列表中
  • 可终结对象在回收时必须进行一些额外的处理,导致程序的运行速度变慢
Finalize方法揭秘

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

*构造一个类型的实例时,如果该类型的Finalize方法是从Object继承的,就不认为这个对象是可以终结的。类型必须重写Object的Finalize方法,这个类型及其派生类的对象才被人为是可以终结的。

垃圾回收器开始时,会查找终结列表中指向垃圾对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到垃圾回收器的另一个内部数据结构freachable队列中,freachable队列中的每个指针都代表其Finalize方法已经准备好的一个对象。

一个特殊的高优先级CLR线程专门负责Finalize方法的调用,当freachable队列为空时,这个线程将会休眠。一旦队列不为空,该线程便会被唤醒,将每一项从freachable队列中移除,并调用每个对象的Finalize方法。

freachable队列

垃圾回收器会将不可达的对象视为垃圾,但是,当垃圾回收器将对象的引用从终结列表移动到freachable队列后,对象将不再被认为是垃圾,其内存不可被回收。标记freachable对象时,这些对象的引用类型的字段所引用的对象也会被递归标记。所有这些对象都会在垃圾回收的过程中存活下来。这时,垃圾回收器结束对垃圾的标识(这个过程中会有某一些被认为是垃圾的对象又被重新认为不是垃圾)。然后垃圾回收器开始压缩可回收内存,特殊的CLR线程清空freachable队列,并执行每个对象的Finalize方法。垃圾回收器下一次调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以这些对象的内存会直接回收。
*整个过程中,可终结的对象需要执行两次垃圾回收才能释放它们占用的内存。

FInalize方法包含共享状态的代码

应该使用线程同步锁,在只有一个终结线程的情况下,可能存在多个CPU分配可终结的对象,但只有一个线程执行Finalize方法,会造成该线程可能跟不上分配速度,造成性能和伸缩性方面的问题。

using语句
static void Main()
{
    using (FileStream fs = new FileStream("Temp.txt", FileMode.Create))
    {
        fs.Write(new byte[] { 1, 2, 3 }, 0, 3);
    }
    File.Delete("Temp.txt");
}

使用using语句的时候编译器会自动生成一个try块和一个Finally块。显然,在Finally块中,编译器会生成代码将变量转型成一个IDisposable并调用Dispose方法。所以using语句只能用于实现了IDisposable接口的类型。

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

GC Handle table
CLR为每个AppDomain都提供了一个GC句柄表,该表允许程序监视对象的生存期,或手动控制对象的生存期。在一个AppDomain创建之初,句柄表是空的。句柄表的每个记录项都包含两种信息:一个指向托管堆上一个对象的指针;一个flag标志,它指出想要监视或控制的对象。

使用GCHandle的Alloc方法控制或监视对象的生存期

public static GCHandle Alloc(object value, GCHandleType type);

GCHandleType枚举

  • Weak:允许监视对象的生存期。可检测垃圾回收在什么时候判定该对象在应用程序代码中即将不可达。此时对象的Finalize可能已经执行也可能没有执行,对象可能仍然存在内存中。
  • WeakTrackResurrection:允许监视对象的生存期。可检测垃圾回收在什么时候判定该对象在应用程序代码中即将不可达。此时对象的Finalize已经执行,对象的内存已回收。
  • Normal:允许控制对象的生存期。告诉垃圾回收器:即将使用应用程序中没有变量引用的对象,该对象必须保留在内存中。垃圾回收发生时,该对象的内存可以压缩(移动)。如果不向Alloc传递任何GCHandleType标志,就默认使用GCHandleType.Normal标志。
  • Pinned:允许控制对象的生存期。告诉垃圾回收器:即将使用应用程序中没有变量引用的对象,该对象必须保留在内存中。垃圾回收发生时,该对象的内存不能压缩(移动)。需要将内存地址传给非托管代码时,这个标志就非常有用。非托管代码可以放心的向托管代码的这个内存写入,知道托管对象的位置不会因为垃圾回收而移动。

调用Alloc方法时,扫描AppDomain的GC句柄表,查找一个可用的记录项来存储传给Alloc的对象地址,并将标志设置为GCHandleType实参传递的值。接着返回GCHandle实例,该实例是一个轻量级的值类型,其中包含一个实例字段,它引用了句柄表中的记录索引项。可以通过获取GCHandle实例,调用其Free方法释放GC句柄表中的记录项,使GCHandle实例无效。

垃圾回收器如何使用GC句柄表
  • 垃圾回收器标记所有可达对象,然后扫描GC句柄表。所有Normal和Pinned都被看成时根,同时标记这些对象(包括这些对象的字段引用的对象)
  • 垃圾回收器扫描GC句柄表,查找所有Weak记录项。如果一个Weak记录项引用了一个未标记的对象,指针标识的就是一个垃圾对象,记录项的指针更改为null
  • 垃圾回收器扫描中结列表,如果列表中的一个指针未引用标记的对象,指针标识的就是一个不可达对象,指针将从终结列表移入freachable队列。这时对象将会被标记,因为对象又变成了可达对象
  • 垃圾回收器扫描GC句柄表,查找所有WeakTrackResurrection记录项。如果一个WeakTrackResurrection记录项引用了一个未标记的对象(由freachable队列中的一个记录项指向的一个对象),指针标识的就是一个垃圾对象,该记录项的指针值更改为null
  • 垃圾回收器对内存进行压缩,其实就是内存碎片整理的过程。某些情况下如果垃圾回收器判断内存碎片化不严重,就会决定不压缩内存。Pinned对象是不会压缩(移动)的,所以垃圾回收器会将其它对象移到它的周围
托管代码的引用传给非托管代码

要使用Pinned标记,因为非托管代码要回调托管代码时,不能真正的将指向一个托管对象的指针传给非托管代码,因为如果发生垃圾回收,对象可能在内存中移动,指针便无效了。为了正常工作,调用Alloc方法,向它传递对象引用和Pinned标记,然后将返回的GCHandle实例转型成为一个Intptr,再将Intptr传递给非托管代码。
非托管代码回调托管代码时,托管代码将Intptr转型成为GCHandle,查询Target属性获得托管对象的引用,非托管代码不再需要这个引用后可以调用GCHandle实例的Free方法,使未来的垃圾回收能释放这些对象。

对象复活

一个被视为垃圾的对象又重新被当做可达(非垃圾)对象的过程,成为对象复活。垃圾回收器将一个对象的引用放入freachable队列,对象就变成可达对象了。待对象的Finalize方法返回,不再有根指向对象,对象才真正死亡。

Finalize方法在执行时将对象指针放到一个静态字段中

class Program
{
    public static Object s_Obj = null;
}

class SomeType
{
    ~SomeType()
    {
        Program.s_Obj = this;
    }
}

上述代码展示了当SomeType的Finalize方法被调用时,该对象的引用会被放入到一个根,使对象得以复活,应用程序可以自由使用这个对象。但需要注意的是,这个对象曾经被终结,所以使用它可能造成无法预测的结果。如果SomeType的一些字段引用了其它对象,这些对象都会被复活,在这些对象中一部分对象的Finalize方法已经被调用过了。

使用ReRegisterForFinalize创建死不了的对象

class SomeType
{
    ~SomeType()
    {
        Program.s_Obj = this;
        GC.ReRegisterForFinalize(this);
    }
}

Finalize方法被调用时,让一个根引用该对象,从而让对象复活。然后调用ReRegisterForFinalize方法,将指定对象(this)的地址添加到终结列表末尾,当垃圾回收器判断这个对象不可达时(s_Obj为null),会将对象的指针从终结列表移动到freachable队列,造成对象的Finalize方法再被调用一次。复活一个对象会复活这个对象引用的所有对象,所有这些对象都需要调用ReRegisterForFinalize方法。

*不建议对象有这种行为

CLR垃圾回收器采用的一种机制,目的是提升应用程序的性能。代的特点:

  • 对象越新,生存期越短
  • 对象越老,生存期越长
  • 回收堆的一部分速度快于回收整个堆

代的工作原理

  • 托管堆在初始化时不包含任何对象,添加到堆的对象成为第0代对象。第0代对象就是新构造的对象,垃圾回收器从未检查过这些对象。
  • CLR初始化时,会为第0代对象选择一个预算容量,假定该容量为256kb,如果分配一个新对象超过预算容量,就会启动一次垃圾回收。不可达的对象将被回收,而在垃圾回收中存活的对象将会被认为时第1代对象。
  • 一次垃圾回收之后第0代就不包含任何对象了,此时新构造的对象会被分配到第0代中。如果此时新分配的对象超过预算容量,将会启动垃圾回收,由于存在第1代对象,所以垃圾回收器也会为第1代对象选择一个预算容量,假设为2M。
  • 开启一次垃圾回收时,垃圾回收器还会检查第1代所占的内存。如果第1代的内存远少于2M,那么垃圾回收器只会检查第0代对象,忽略第1代对象从而加快垃圾回收的速度。
  • 忽略第1代对象的好处在于:垃圾回收时不必遍历托管堆中的每个对象。如果一个对象引用了老一代的对象,垃圾回收器就可以忽略老对象内部的所有引用。
  • 基于较老的对象生存周期较长的特点,垃圾回收器会认为检查第1代中的对象很有可能找不到多少垃圾,回收不了多少内存,因此对第1代进行垃圾回收很有可能浪费时间,会出现如果第1代真的有垃圾而没有回收的情况。
  • 随着程序的运行,假设第1代的内存已经超出2M,将会检查第0代和第1代的对象,两代都会被垃圾回收,之前可能残留在第1代中不可达的对象将会在这时被回收。
  • 垃圾回收后第0代中存活的对象变成第1代,而第1代中存活的对象将变成第2代(没有第三代)。并且为第2代选择约为10M的预算容量,预算的大小是为了提升性能,预算越大启动垃圾回收的频率就越低。

*如果垃圾回收在第0代后存活的对象很少,垃圾回收器可能会将第0代的预算从256kb降低到128kb。意味着垃圾回收更加的频繁,但垃圾回收器需要做的工作会减少,从而减小进程的工作集。如果第0代所有对象都是垃圾,垃圾回收时就没必要压缩内存,只需让NextObjPtr指针指向第0代的起始位置即可,这样将变得更快。反之垃圾回收器也有可能增大预算,使垃圾回收的次数变少,但每次回收的内存要多得多。如果没有回收足够的内存,垃圾回收器便会执行一次完整的回收,如果内存还是不够便会抛出OutOfMemoryException异常。

使用MemoryFailPoint预测需求大量内存的操作能否成功
try
{
    //保留1G内存
    using(MemoryFailPoint mfp = new MemoryFailPoint(1000))
    {
        //执行消耗大量内存的算法
    }  //Dispose释放1G内存
}
catch (InsufficientMemoryException e)
{
    //无法保留所需的内存
}
编程控制垃圾回收器

强迫执行一次垃圾回收可调用以下方法

  • public static void Collect():对所有代执行一次完全回收。
  • public static void Collect(int generation):允许回收指定的代,可传递0到GC.MaxGeneration之间的任何整数(GC.MaxGeneration最大为2,因为最多只有2代)。传递0回收第0代对象;传递1回收第0代和第1代对象;传递2回收第0,1,2代对象。
  • public static void Collect(int generation, GCCollectionMode mode):与第2个方法的差别在于多了一个GCCollectionMode参数

GCCollectionMode枚举

  • Default:等同于不传递任何符号
  • Forced:强迫回收指定的代以及低于它的所有代
  • Optimized:只有在能够释放大量内存或者减少碎片化的前提下,才执行回收。如果垃圾回收效果不佳,当前调用将不产生作用

###### GC.WaitForPendingFinalizers
挂起当前线程,直到处理freachable队列的线程清空该队列,完成对每个对象的Finalize方法的调用。

示例代码:

class MyFinalizeObject
{
    ~MyFinalizeObject()
    {
        Console.WriteLine("MyFinalizeObject 的 Finalize方法被执行");
        GC.KeepAlive(this);
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("最大代数:{0}", GC.MaxGeneration);

        MyFinalizeObject o = new MyFinalizeObject();

        //查看对象o所属的代
        Console.WriteLine("对象o当前代数:{0}", GC.GetGeneration(o));

        //执行垃圾回收提升对象的代
        GC.Collect();
        Console.WriteLine("1次垃圾回收后对象o当前代数:{0}", GC.GetGeneration(o));

        GC.Collect();
        Console.WriteLine("2次垃圾回收后对象o当前代数:{0}", GC.GetGeneration(o));

        GC.Collect();
        Console.WriteLine("3次垃圾回收后对象o当前代数:{0}", GC.GetGeneration(o));

        //销毁对象的引用
        o = null;

        //回收第0代
        Console.WriteLine("回收第0代对象");
        GC.Collect(0);
        GC.WaitForPendingFinalizers(); //对象o的Finalize方法未被调用

        //回收第1代
        Console.WriteLine("回收第1代对象");
        GC.Collect(1);
        GC.WaitForPendingFinalizers(); //对象o的Finalize方法未被调用

        //回收第2代
        Console.WriteLine("回收第2代对象");
        GC.Collect(2);
        GC.WaitForPendingFinalizers(); //对象o的Finalize方法被调用
        Console.ReadKey();
    }
}

运行结果
image.png

线程劫持

CLR要开始一次垃圾回收时,会立即挂起正在执行托管代码的所有线程。然后CLR检查每个线程的指令指针,判断线程执行到了哪里。接着指令指针和JIT编译器生成的表进行比较,判断线程正在执行什么代码。
如果线程的指令指针恰好在一个表中标记好的便宜位置,就说该线程抵达了一个安全点。线程可以在安全点安全的挂起,直到垃圾回收结束。反之则表示线程不在安全点,CLR则不能执行垃圾回收。在这种情况下CLR会劫持该线程。也就是说它会修改线程栈,使它返回的地址指向CLR内部实现的一个特殊函数。然后线程恢复执行,当执行的方法返回后,特殊函数开始执行,它会将线程挂起。
然而,线程有时候会长时间不能从当前方法返回。所以当线程恢复执行后,CLR会用大约250ms的时间尝试劫持线程,过了这个时间,CLR会再次挂起线程,并检查它的指令指针。如果线程抵达了安全点,那么就可以开始垃圾回收了。但是如果线程还未抵达安全点,CLR就检查是否调用了另一个方法,如果是,CLR再一次修改线程栈,以便从最近执行的一个方法返回后劫持线程。然后CLR恢复线程进行下一次劫持尝试。
所有线程都抵达安全点或是被劫持后,垃圾回收才能开始,垃圾回收完成后,所有线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。

*在实际应用中,CLR主要通过劫持线程进而挂起线程,而不是根据JIT编译器生成的表来判断线程是否抵达一个安全点。原因是JIT生成表需要大量的内存,而且会增大工作集,进而严重影响性能。

垃圾回收模式
  • CLR在启动时会选择一种GC模式,在进程的生存期内这个模式不能改变。两种基本的GC模式:
  • 工作站:客户端应用程序优化垃圾回收器。垃圾回收器假定机器上运行的其它应用程序对CPU资源的要求不高。工作站模式有两个子模式:并发回收器的工作站、无并发回收器的工作站。
  • 服务器:服务端应用程序优化垃圾回收器。垃圾回收器假定机器上没有运行其它应用程序,并假定机器的所有CPU都可用来执行垃圾回收。该GC模式造成托管堆分解成几个区域,每个CPU一个区域。开始一次垃圾回收时,垃圾回收器在每个CPU上都会运行一个线程,每个线程和其它线程并发回收它自己的区域。这个功能要求应用程序在多CPU计算机上运行,使线程真正的同时工作,从而提升性能。

工作站GC模式可以使用并发或非并发的方式运行,在并发方式中,垃圾回收器有一个额外的后台线程,能在应用程序运行时并发地回收对象。一个线程因为分配一个对象导致第0代超出预算,垃圾回收器会先挂起所有线程,再判断要回收哪些代。如果垃圾回收器需要回收的是第0代或第1代,那么将没什么不同。如果要回收第2代,那么就会增大第0代的预算,以便在第0代中分配新对象。

设置GC模式
使用GCSettings类的LatencyMode属性控制对垃圾回收模式

GCLatencyMode枚举

  • Batch(服务器GC模式的默认值):在工作站GC模式中,这个延迟模式关闭并发GC。在服务器模式中,这是唯一有效的延迟模式。
  • Interactive(工作站GC模式的默认值):在工作站GC模式中,这个延迟模式会打开并发GC。在服务器GC中,这个延迟模式是无效的。
  • LowLatency:在工作站GC模式中,在短期的,时间敏感的操作中使用这个延迟模式;在这种情况下对第2代的回收可能造成混乱。在服务器GC中,这个延迟模式是无效的。补充说明:一般情况下这个模式用来执行一次短期的、时间敏感的操作,再将模式改为Batch或Interactive。这个模式期间垃圾回收器会尽量避免回收第2代对象,如果调GC.Collect或者Windows告诉CLR系统内存低第2代仍然会被回收。在这个模式应用程序抛出OutOfMemoryException的几率会增大,所以尽可能短地处在这个模式中,避免分配太多地对象或大对象。

大对象
任何85000字节或更大的对象都自动视为大对象。大对象从一个特殊的大对象堆中分配。由于在堆中下移85000字节的内存块会浪费太多的CPU时间,所以大对象永远不会被压缩。但是也不能假定大对象永远不移动,因为在未来的某个时刻,可能大对象已经不再是85000字节了。

*大对象总是被认为是第2代的一部分,所以只能为需要长时间存活的资源创建大对象。如果分配短时间存活的大对象,将导致第2代更加频繁的回收,造成性能损害。

证明大对象总是在第2代中被分配

static void Main()
{
    object o = new byte[85000];
    //显示2而不是0
    Console.WriteLine(GC.GetGeneration(o));
}
监视垃圾回收
  • public static long GetTotalMemory(bool forceFullCollection):查看托管堆中的对象当前使用了多少内存
  • public static int CollectionCount(int generation):查看指定代发生了多少次垃圾回收

*可通过使用这两个方法把握代码块对进程工作集的影响,并了解执行代码时发生了多少次垃圾回收,如果数字太高则需要考虑优化代码

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 2020-12-22

异常处理

什么时候应该抛出异常

当一个类型的行动成员不能正在完整行动任务时,就应该抛出异常通知调用者。

*行动成员指类型本身或者类型实例可以执行的操作,如C#中StringBuilder中定义的Append,Insert等

捕捉异常代码结构
private void DoSomething()
{
    try
    {
        //将可能发生异常的代码放在这里
    }
    catch (InvalidOperationException)
    {
        //捕捉到InvalidOperationException异常,对应的处理代码放在这里
    }
    catch (IOException)
    {
        //捕捉到IOException异常,对应的处理代码放在这里
    }
    catch (Exception)
    {
        //捕捉到除了上述之外的其它异常,对应的处理代码放在这里

        //这里是将异常抛出
        throw;
    }
    finally
    {
        //这里的代码总是被执行
    }

    //如果try块没有抛出异常或者某个catch捕捉到异常却没有抛出就执行以下的代码,否则以下代码不执行
}
try块

try块中包含的是可能会发生异常的代码,异常恢复代码应放在一个或多个catch块中。针对应用程序安全的恢复某一种异常都需要有一个对应的catch块。一个try块至少要关联一个catch块或finally块,单独一个try块C#是不允许的,而且这样也没有意义。

*如果一个try块中包含执行多个可能抛出同一个异常类型的操作,但不同的操作对应的恢复措施不同,我们就应该将这些操作拆分到它自己的try块中,以保证正确的恢复状态

catch块

catch块包含的是响应一个异常需要执行的代码。一个try块可以关联0个或多个catch块。如果try块中的代码没有异常发生,CLR永远不会执行catch块中的代码。线程将跳过所有catch块,直至finally块(里面有代码的话)中的代码。catch关键字后面圆括号中的表达式称为捕捉类型,即要捕捉的异常类型。

*使用Visual Studio调试catch块时,可以在监视窗口中添加特殊的变量名称$exception来检查当前抛出异常的对象

catch块检索顺序与注意事项

CLR是自上而下检索一个匹配的catch块,所以编程的时候应注意将派生程度最大的异常类型放在顶部,接着是它们的基类,最后才是System.Exception,如果顺序没有放对,例如将最具体的异常类型放在了最底部的catch块中,C#编译器将会发生错误,因为这个catch块无法被执行到。

一旦try块中的代码发生异常,而没有与之匹配的catch块的话,CLR会去调用栈的更高一层搜索与之匹配的异常类型,如果到了栈的顶部还是没有找到,就会发生一个未处理的异常。

在catch块的末尾可以做的事情
  • 重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生
  • 抛出一个不同的异常,向调用栈高一层的代码提供更加丰富的异常信息
  • 让线程从catch块的底部退出

前两种技术CLR将回溯调用栈,查找捕捉类型与抛出异常的类型匹配的catch块并抛出一个异常。
如果选择让线程从catch块的底部退出,将立即执行包含在finally块中的代码,完毕后执行紧跟在finally块之后的语句。如果不存在finally块,线程将从最后一个catch块之后的语句开始执行。

finally块

finally块中的代码是保证一定会执行的代码且一定要在所有catch块的后面,通常包含的是对try块中的行动所要求的资源清理操作。一个try块最多只能关联一个finally块。

private void ReadFile(string path)
{
    FileStream fs = null;
    try
    {
        fs = new FileStream(path, FileMode.Open);
        //处理文件数据...
    }
    catch (IOException)
    {
        //IOException异常恢复代码
    }
    finally
    {
        //确保文件关闭
        if (fs != null)
            fs.Close();
    }
}

上述代码,无论try块代码有没有发生异常,文件都一定会被关闭。如果将关闭文件的代码放在finally块语句之后是不正确的,因为如果抛出异常但没有捕捉到,finally块之后的语句将永远不会被执行,直到下一次垃圾回收才会关闭文件。

*一般情况下catch块和finally块中的代码应只有一两行

System.Exception类属性介绍
  • 只读属性Message:String类型,指出抛出异常的原因
  • 只读属性Data:IDictionary类型,代码会在抛出异常之前在该集合中添加一个记录项
  • 可读可写属性Source:String类型,包含生成异常的程序集的名称
  • 只读属性StackTrace:String类型,包含抛出异常之前调用过的所有方法的名称和签名,有助于调试代码
  • 只读属性TargetSite:MethodBase类型,包含抛出异常的方法
  • 只读属性HelpLink:String类型,包含异常文档的URL,不建议使用
  • 只读属性InnerException:Exception类型,通常值为null,如果当前异常是在处理一个异常时抛出的,那么该属性就指出前一个异常是什么
正确使用异常类
  • 善用finally块,常用于显示释放对象,避免资源泄露
  • 维持状态,发生不可恢复的异常时回滚部分完成的操作
  • 在一个线程中捕捉异常,在另一个线程中重新抛出异常
  • 合理的从异常中恢复状态
private string SomeMothods()
{
    var result = "";
    var a = 1;
    var b = 0;
    try
    {
        a /= b;
    }
    catch (DivideByZeroException)
    {

        result = "被除数不能为0";
    }

    return result;
}
未处理异常

异常抛出时,CLR会在调用栈中向上查找与抛出异常对象的类型匹配的catch块。如果没有找到,就会发生一个未处理的异常。当CLR检测到进程中的任意一个线程有未处理的异常,都会终止进程。发生未处理异常表明程序遇到了未预料的情况。同时,发生未处理的异常时windows会向事件日志写入一条记录,可以打开事件查看器查看
image.png

异常设置
可通过调试菜单打开异常设置窗口如图
image.png

展开Common Languages Runtime Exceptions可以查看Visual Studio能够识别的异常类型
image.png

如果勾选了异常类型的复选框,调试器就会在抛出该异常的时候中断,此时CLR不会查找任何与之匹配的catch块。
如果异常类的复选框没有勾选,调试器只有在该异常类型未得到处理时才会中断。

通过添加操作还可以添加自定义的异常类型
image.png

异常处理的性能问题
  • 非托管c++编译器:必须生成代码来跟踪哪些对象被构造成功,编译器还必须生成代码用来在一个异常被捕捉到的时候,调用每一个已经成功构造的对象的析构器。如此便会在程序中生成大量的bookkeeping代码,对代码的大小和执行时间都会造成负面影响。
  • 托管编译器:由于托管对象是在托管堆中分配的,而托管堆受到垃圾回收的监视。如果一个对象成功构造并且抛出一个异常,垃圾回收器最终会释放对象的内存。编译器无需生成任何bookkeeping代码来跟踪成功构造的对象,也无需保证析构器的调用。与托管c++相比,意味着生成的代码更少,运行要执行的代码更少,应用程序的性能更好。
查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2020-12-21

命名空间

命名空间与类型有些相似,在类型中可以声明诸多成员:字段,属性,方法,而在命名空间中可以声明:类,接口

命名空间属于逻辑组合,用来对类型的功能进行逻辑分组

成员根据相关性可以将name, age, sex成员定义到Person类型中
类型可以把Dog, Cat类型定义到名为Animal的命名空间里

声明命名空间的结构
namespace 名字 { }
命名空间不能有修饰符如访问修饰符
//错误,不能使用修饰符
private namespace 名字 { }
声明Animal命名空间
namespace Animal { }

*命名空间的命名规则可参考变量名(可查看往期文章变量和常量)

命名空间中不能包含成员: 字段,属性,方法
namespace Animal
{
    private int id; //错误
    public int Id { get; set; } //错误
    private void Test() { } //错误
}
类型名必须唯一,同一个命名空间中无法定义同样的类型
namespace Animal
{
    //正确
    class Dog { }

    //错误,命名空间 Animal中已经包含了Dog的定义
    class Dog { }
}

*如果未给类型显示提供命名空间,该类型将会被编译器默认放到一个没有名称的全局命名空间(global namespace)中

//正确
class Dog { }
//错误,命名空间 global namespace中已经包含了Dog的定义
class Dog { }

*就像类型中无法定义多个相同名称的成员一样,类型名也需要具有唯一性

命名空间的另一个作用便是提高类型名的唯一性,无法在同一个名称空间下定义相同的类型,本质原因也是类型名需唯一

所以可以写出下面这样的代码

//正确
class Dog { }
namespace Animal
{
    //正确,属于不同命名空间
    class Dog { }
}

原因: 两个Dog类型分属不同的名称空间,属于全局命名空间的Dog类型名:Dog,属于Animal命名空间的Dog类型名: Animal.Dog

访问不同命名空间中的类型(全局命名空间类型除外)需要使用类型限定名,同一个命名空间下则不需要

namespace Animal
{
    class Dog
    {
        void Test()
        {
         //正确
         Dog dog = new Dog();
        }
    }
}
namespace A
{
    class Person
    {
        void Test()
        {
            //错误,未找到类型名
            Dog dog = new Dog();
            //正确
            Animal.Dog dog = new Animal.Dog();
        }
    }
}

但是这样的代码过于繁琐,同时也降低了代码的可读性

可以使用using指令简化代码

using Animal;
namespace Animal
{
    class Dog { }
}
namespace A
{
    class Person
    {
        void Test()
        {
            //正确
            Animal.Dog dog = new Animal.Dog();
            //正确
            Dog dog = new Dog();
        }
    }
}

using指令注意事项

namespace A
{
    //正确
    using Animal;
    class Person { }
}
//错误用法
namespace A
{
    class Person { }
    //错误,using指令必须在命名空间中定义的所有其它元素之前
    using Animal;
    class Person { }
}

为什么以下代码会报错呢

//错误
using Animal;
namespace Animal { }

由于未显示指定命名空间的类型默认添加到全局没有名字的命名空间中
所以上述代码相当于

//global只是举例
namespace global
{
    class Dog { }

    //错误,using指令必须在命名空间中定义的所有其它元素之前
    using Animal;
    namespace Animal { }
}

*这便是为什么所有using都需要写在最前面的原因了

可以定义同名的命名空间
namespace Animal
{
    class Dog { }
}
namespace Animal
{
    class Cat { }
}

相当于是这样的

namespace Animal
{
    class Dog { }
    class Cat { }
}
命名空间可以嵌套
namespace A
{
    namespace B
    {
        namespace C
        {
        }
    }
}

也可以从外到内依次递进,使用句点分隔每个命名空间名称的方式改写简化代码

namespace A.B.C { }
不明确的引用

如果不同的命名空间中定义了相同的类型,这时在使用该类型的时候便会出现歧义,编译器报错"类型"是不明确的引用

namespace A
{
    //命名空间A定义Bird类型
    class Bird { }
}
namespace B
{
    //命名空间B定义Bird类型
    class Bird { }
}
//只添加using A或者using B是正确的
namespace C
{
    using A;
    class Person
    {
        void Test()
        {
            //正确
            Bird bird = new Bird();
        }
    }
}

同时使用两个命名空间下的相同类型名便会出现歧义

//若需要同时使用两个类型
namespace C
{
    using A;
    using B;
    class Person
    {
        void Test()
        {
            //错误Bird 是A.Bird 和B.Bird 之间不明确的引用
            Bird bird = new Bird();
        }
    }
}
消除歧义性方法
  • 使用类型的完全限定名
A.Bird birdA = new A.Bird();//正确
B.Bird birdB = new B.Bird();//正确
  • 创建类型别名
namespace C
{
    //为A.Bird定义别名为BirdA 
    using BirdA = A.Bird;
    //为B.Bird定义别名为BirdB
    using BirdB = B.Bird;
    class Person
    {
        void Test()
        {
            BirdA birdA = new BirdA(); //相当于A.Bird birdA = new A.Bird()
            BirdB birdB = new BirdB(); //相当于B.Bird birdB = new B.Bird()
        }
    }
}
  • 创建命名空间别名
namespace A.B.C
{
    class Bird
    {
        public string Name { get; set; }
    }
}
namespace D.E.F
{
    class Bird
    {
        public string Name { get; set; }
    }
}
namespace C
{
    //命名空间别名
    using A = A.B.C;
    using D = D.E.F;
    class Person
    {
        void Test()
        {
            A::Bird birdA = new A::Bird() { Name = "一只黑色的鸟" };
            D::Bird birdD = new D::Bird() { Name = "一只白色的鸟" };
        }
    }
}

*最后一点建议: 虽然可以通过创建类型别名或命名空间别名的方式消除歧义性,但还是建议定义类型时使类型名称唯一

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2020-12-16

可空值类型

可空值类型

引用类型的变量可以为空,值类型的变量则不被允许赋值为null,而可空值类型便是可以赋值为null的值类型。

可空值类型的声明与初始化
int? x = null;
int? y = 1;
类型之间的转换
  • int转int?总是会成功
int? a = 5;     //正确
  • int?转int需要显示转换
int c = a;      //错误,无法从int?隐式转换为int
int c = (int)a; //正确
  • 可空基元类型之间的转型
int? b = null;
//d=5
double? d = 5;
//e=null
double? e = b;
C#对可空值类型应用操作符规则
  • 一元操作符(++,+,-,--,!,~):操作数是null,结果就是null
int? a = null;
Console.WriteLine(a++);     //null
Console.WriteLine(a + 10);  //null
  • 二元操作符(+,-,*,/,%,&,|,^,<<,>>):两个操作数其中一个为null,结果就为null。特例:&,|和Boolean?类型的操作数时,情况如下
bool? a = null;
bool? b = true;
Console.WriteLine(a & b);   //结果为null
Console.WriteLine(a | b);   //结果为true

bool? c = false;
Console.WriteLine(a & c);   //结果为false
Console.WriteLine(a | c);   //结果为null
  • 相等性操作符(==,!=):两个操作数皆为null,两者相等;一个为null,一个不为null,则不等;皆不为null,则比较数值是否相等。
int? a = null;
int? b = null;
int? c = 10;
Console.WriteLine(a == b);  //true
Console.WriteLine(a == c);  //false
  • 关系操作符(<,>,<=,>=):两个操作数任意一个为null,返回false。两个操作数都不是null,就比较值返回结果。
int? a = null;
int? b = null;
int? c = 10;
Console.WriteLine(a > b);   //false
Console.WriteLine(a > c);   //false
空接合操作符(??)

??:获取两个操作数,如果左边的操作数不为null,则返回左边操作数的值,反之返回右边操作数的值。

int? a = null;
//等价于int b = z.HasValue ? z.Value :123
int b = a ?? 123;
Console.WriteLine(b); //结果为123

int? c = 3;
Console.WriteLine(c ?? 123); //结果为3
可空值类型的装箱与拆箱
  • 装箱:CLR对可空值类型装箱时,如果实例为null,不装箱任何东西,并返回null,否则(以int?为例)则会装箱一个Int32的值
int? a = null;
object o = a;
Console.WriteLine(o == null); //true

a = 1;
o = a;
Console.WriteLine(o); //结果为1
Console.WriteLine(o.GetType()); //输出System.Int32
  • 拆箱:如果已装箱值类型的引用是null,将其拆箱成一个可空值类型(T?),CLR会直接将T?的值设为null
object o = null;
//a=null
int? a = (int?)o;
Console.WriteLine(a == null); //输出true
//引发System.NullReferenceException异常
int b = (int)o;
可重载自定义值类型的操作符方法,从而让编译器正确对待它
struct Position
{
    private int m_X, m_Y;
    public Position(int x, int y)
    {
        this.m_X = x;
        this.m_Y = y;
    }

    public static bool operator ==(Position pos1, Position pos2)
    {
        return (pos1.m_X == pos2.m_X) && (pos1.m_Y == pos2.m_Y);
    }

    public static bool operator !=(Position pos1, Position pos2)
    {
        return (pos1.m_X != pos2.m_X) || (pos1.m_Y != pos2.m_Y);
    }
}

static void Main(string[] args)
{
    Position? pos1 = new Position(1, 2);
    Position? pos2 = new Position(3, 4);
    Position? pos3 = new Position(3, 4);
    Console.WriteLine(pos1 == pos2); //false
    Console.WriteLine(pos1 != pos2); //true
    Console.WriteLine(pos2 == pos3); //true
}
查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2020-12-09

Attribute

attribute
  • 可应用于类型和成员
  • 是类的一个实例
  • 从System.Attribute派生
C# attribute使用范围
  • 程序集
  • 模块
  • 类型(类、结构、枚举、接口、委托)
  • 字段
  • 方法(含构造器)
  • 方法参数
  • 方法返回值
  • 属性
  • 事件
  • 泛型类型参数
FCL定义的Attribute列举
  • Serializable:应用于类型,告诉序列化格式化器,一个实例的字段可以被序列化和反序列化
  • Flags:应用于枚举类型,将枚举类型作为一个位标志(bit flag)集合使用
默认自定义的attribute类能用于任何目标元素
public class MyAttribute : Attribute
{
    public MyAttribute()
    {

    }
}

[MyAttribute]
class Program
{
    [MyAttribute]
    static void Main(string[] args)
    {
    }
}

限定attribute的使用范围:枚举类型

[AttributeUsage(AttributeTargets.Enum, AllowMultiple = false, Inherited = false)]
public class MyEnumAttribute : Attribute
{
    public MyEnumAttribute()
    {

    }
}

以上代码利用AttributeUsage告知编译器定制attribute的合法应用范围,如果将定制attribute应用于一个无效目标时将会报错

[MyEnumAttribute] //正确
public enum MyEnum { }

[MyEnumAttribute] //错误:只对枚举有效
class Program
{
}
AllowMultiple和Inherited
  • AllowMultiple:不显示设置为true就只能向一个选定目标应用一次
  • Inherited:指明在attribute应用于基类的时候是否同时应用于派生类和重写方法上
[AttributeUsage(AttributeTargets.Enum, AllowMultiple = false, Inherited = false)]
public class MyEnumAttribute : Attribute
{
    public MyEnumAttribute()
    {

    }
}

//错误:特性重复
[MyEnumAttribute][MyEnumAttribute]
public enum MyEnum { }

/*------------------------------*/

[AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = false)]
public class MyEnumAttribute : Attribute
{
    public MyEnumAttribute()
    {

    }
}

//正确
[MyEnumAttribute][MyEnumAttribute]
public enum MyEnum { }

定制attribute时可以使用构造器获取参数,在使用过程中,必须传递一个编译时常量表达式。传递参数规则:

  • Type类型参数:必须使用C#的typeof操作符传递
  • Object参数:可传递int、string或其它任意常量表达式(包括null),如果常量表达式为值类型,那么会在运行时构造attribute实例的时候对其装箱

示例代码:

/// <summary>
/// 自定义Attribute
/// </summary>
/// <param name="name">引用string</param>
/// <param name="o">任意类型,有必要就进行装箱</param>
/// <param name="t">Type类型</param>
public MyAttribute(string name, Object o, Type t)
{

}

class Program
{
    [MyAttribute("name", 12, typeof(string))]
    static void Main(string[] args)
    {
    }
}
利用反射检查类型attribute改变代码的行为
[AttributeUsage(AttributeTargets.Enum, AllowMultiple = false, Inherited = false)]
public class MyEnumAttribute : Attribute
{
    public MyEnumAttribute()
    {

    }
}

[MyEnumAttribute]
public enum MyEnum { }

public void Test(Type enumType)
{
    if(enumType.IsDefined(typeof(MyEnumAttribute), false)){
        //如果含有MyEnumAttribute特性执行以下代码
    }
    else
    {
        //否则执行以下代码
    }
}
使用反射检查方法attribute特性
public class MyAttribute : Attribute
{
    public MyAttribute(string name, Object o, Type t) {}
}

[DebuggerDisplayAttribute("doubleJ", Name = "Name", Target = typeof(Program))]
public class Program
{
    [Conditional("Debug")]
    public void DoSomething() { }

    [MyAttribute("name", 12, typeof(string))]
    public static void Main(string[] args)
    {
        ShowAttribute(typeof(Program));

        var members = typeof(Program).FindMembers(
            MemberTypes.Method | MemberTypes.Constructor,
            BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static,
            Type.FilterName, "*"
        );

        foreach (var member in members)
            ShowAttribute(member);

        Console.ReadKey();
    }

    private static void ShowAttribute(MemberInfo memberInfo)
    {
        var attributes = Attribute.GetCustomAttributes(memberInfo);
        foreach (var attribute in attributes)
        {
            Console.WriteLine(attribute.GetType().ToString());

            if (attribute is MyAttribute)
            {

            }

            if (attribute is ConditionalAttribute)
            {

            }

            DebuggerDisplayAttribute dda = attribute as DebuggerDisplayAttribute;
            if (dda != null)
                Console.WriteLine("value = {0}, name = {1}, target = {2}", dda.Value, dda.Name, dda.Target);
        }
    }
}

运行结果
image.png

IsDefined、GetCustomAttributes、GetCustomAttribute对比
  • IsDefined:有一个指定的Attribute派生类的实例与目标关联就返回true,由于不构造任何Attribute类的实例所以效率很高
  • GetCustomAttributes:返回一个数组,每个元素都是应用于目标的指定Attribute类的一个实例。如果不为该方法指定具体的Attribute,数组中已经应用的所有Attribute的实例,如果没有应用任何Attribute类的实例返回空数组
  • GetCustomAttribute:返回应用于目标的指定Attribute类的一个实例。如果没有则返回null。如果应用了多个Attribute实例,则抛出异常

*使用IsDefined不会调用Attribute的构造器,也不会设置它的字段和属性,所以IsDefined效率最高,如果想要知道一个Attribute是否应用于一个目标,那么应该选择使用该方法

条件Attribute

使用条件Attribute后它的执行便会依赖于指定的预处理标识符,如:

[Conditional("DEBUG")]
[Conditional("TEST")]
public class CondAttribute : Attribute
{

}

*当编译器发现目标元素使用了Conditional的一个实例,那么在含有目标元素的代码在进行编译时(以上述代码为例),只有在定义了TEST或DEBUG符号的前提下,编译器才会在元数据中生成attribute信息,尽管如此,但attribute类的定义元数据和实现仍然存在于程序集中。

重写Match和Equals实现两个attribute实例的匹配
[Flags]
public enum Role
{
    Read = 0x0001,
    Write = 0x0002
}

sealed class RoleAttribute : Attribute
{
    private Role m_Role;

    public RoleAttribute(Role role)
    {
        this.m_Role = role;
    }

    public override bool Match(object obj)
    {
        if (obj == null) return false;

        if (this.GetType() != obj.GetType()) return false;

        RoleAttribute other = (RoleAttribute)obj;
        if ((other.m_Role & this.m_Role) != this.m_Role)
            return false;

        return true;
    }

    public override bool Equals(object obj)
    {
        if (obj == null) return false;

        if (this.GetType() != obj.GetType()) return false;

        RoleAttribute other = (RoleAttribute)obj;
        if (other.m_Role != this.m_Role)
            return false;

        return true;
    }

    public override int GetHashCode()
    {
        return (Int32)this.m_Role;
    }
}

[RoleAttribute(Role.Read)]
class Role1 { }

[RoleAttribute(Role.Write)]
class Role2 { }

public class Program
{
    public static void Main(string[] args)
    {
        CanRead(new Role1());
        CanRead(new Role2());
        Console.ReadKey();
    }
}

private static void CanRead(object obj)
{
    Attribute read = new RoleAttribute(Role.Read);
    Attribute validRole = Attribute.GetCustomAttribute(obj.GetType(), typeof(RoleAttribute), false);
    if((validRole!=null)&&read.Match(validRole))
        Console.WriteLine("{0} can read", obj.GetType());
    else
        Console.WriteLine("{0} can not read", obj.GetType());
}

输出结果
image.png

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 2 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-11-18
个人主页被 1.1k 人浏览