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进程图

上图所示有两个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_第1张图片

代码分析:
首先获得一个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();
            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_第2张图片

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异常。