什么是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过程图

上图所示有两个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();    }}

运行后果

代码剖析:
首先取得一个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();    }}

输入后果

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一去不复返了,如此服务器是不是应该创立更多的线程,而这些线程自身也可能进入有限循环。

宿主可利用线程终止性能解决上述问题,线程终止工作形式如图:

  • 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异样。